mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-09 06:35:44 +08:00
Compare commits
1986 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a3140f48b4 | ||
|
335883f9f0 | ||
|
f2b1b88242 | ||
|
4163ee484c | ||
|
a14c3f81b0 | ||
|
f08d2c998d | ||
|
1b0cf938d9 | ||
|
03d714f2d4 | ||
|
4feef53cb0 | ||
|
397e6a9d57 | ||
|
9df2ceba84 | ||
|
1b65dc7256 | ||
|
4ffadab4c5 | ||
|
7ed90ff7f8 | ||
|
ec79126b4a | ||
|
ee13bb559e | ||
|
ffbc0cdff4 | ||
|
801b79b728 | ||
|
cd1d719a92 | ||
|
cf3848f15d | ||
|
df5d43283b | ||
|
91f0f9f65e | ||
|
708e5d1941 | ||
|
8773117e50 | ||
|
8840e63c1c | ||
|
4e2d9d6acd | ||
|
94259f9515 | ||
|
5b0ff01cbf | ||
|
3ae1109a31 | ||
|
f50b9655ae | ||
|
081daee53f | ||
|
95123aceb5 | ||
|
2a7b22c96f | ||
|
c1b15490c1 | ||
|
f8cb84a706 | ||
|
063f9034a1 | ||
|
9c1b2ff89c | ||
|
44c05d9fd9 | ||
|
dd3c149e98 | ||
|
0623fe4dc3 | ||
|
2746ff68c8 | ||
|
62eb070c1b | ||
|
3b8147b6ad | ||
|
221b732472 | ||
|
132641f269 | ||
|
d8cf585fc5 | ||
|
7f0d9952aa | ||
|
415b4879f1 | ||
|
7e5d6ef9b6 | ||
|
ff297957c7 | ||
|
7489f5e62d | ||
|
3575c16326 | ||
|
04f4f5f713 | ||
|
eb78af7c77 | ||
|
114d5ab541 | ||
|
f13140e39e | ||
|
97717c648e | ||
|
ec6705eaf4 | ||
|
1e11eff811 | ||
|
13fb8c5d7d | ||
|
e87e06dd37 | ||
|
b9bb79b4dc | ||
|
d0f907f349 | ||
|
867c83b84c | ||
|
632d389411 | ||
|
b4df1b6e42 | ||
|
176620e3bb | ||
|
c37f22dc65 | ||
|
9d9cf27460 | ||
|
6e8def3ef7 | ||
|
ec0d872f11 | ||
|
9e4a8708e7 | ||
|
a681fdeee1 | ||
|
8763a76475 | ||
|
4306fba997 | ||
|
3759239dac | ||
|
4ec0b1d6e4 | ||
|
6271726f07 | ||
|
e5740579f4 | ||
|
ca97e3a3e6 | ||
|
d3f6822080 | ||
|
9d9a6dfddb | ||
|
c3e24d7b96 | ||
|
20d163cf3a | ||
|
cf90f3abc9 | ||
|
8871660f0e | ||
|
97a030c22e | ||
|
8a6ce0f0db | ||
|
285b6b9287 | ||
|
3515cc8e63 | ||
|
b0132c9718 | ||
|
ef84c31761 | ||
|
4953d4b4d0 | ||
|
2833718c90 | ||
|
ab7775e1ef | ||
|
a5c4562f59 | ||
|
00bd5b0e1e | ||
|
b66c07bd96 | ||
|
d8d0a59371 | ||
|
953153ea1a | ||
|
fc03ce1247 | ||
|
08bf70681c | ||
|
00bc6a6301 | ||
|
c5916cf5ec | ||
|
4213a5fad1 | ||
|
17fc9cf0eb | ||
|
dce72a16f0 | ||
|
7550c2321c | ||
|
f0c4360210 | ||
|
d10323701a | ||
|
94c437c3e3 | ||
|
202fb19ab1 | ||
|
2bb3498602 | ||
|
9d476d7add | ||
|
014829e69a | ||
|
a127cd6444 | ||
|
486ffbdc08 | ||
|
526c5bdd91 | ||
|
db11651f85 | ||
|
4ce7d9f7a9 | ||
|
df3c296850 | ||
|
5ef9220175 | ||
|
e3c52781f3 | ||
|
1cb1946497 | ||
|
9e486e6836 | ||
|
e5dc4a55b0 | ||
|
76d04805a4 | ||
|
652a20c523 | ||
|
6c2af1bef5 | ||
|
ba2aa9d6e7 | ||
|
a1c72fe780 | ||
|
197f9fd964 | ||
|
fc2c10bb3f | ||
|
d7dd797494 | ||
|
4f954657a1 | ||
|
3eddcafeb6 | ||
|
3df4718709 | ||
|
b5fddf8fa1 | ||
|
fdc8a83f25 | ||
|
e79121e46a | ||
|
aab71e55de | ||
|
caf4ee6863 | ||
|
30b0c45539 | ||
|
2ab75db9c9 | ||
|
f9009de894 | ||
|
da8ed1ac5a | ||
|
afde7e4a44 | ||
|
3e94adb4b1 | ||
|
5c577e533f | ||
|
c64b9d5a2b | ||
|
545ea16ae8 | ||
|
bf245a4ee1 | ||
|
b28fc26ae6 | ||
|
750dfdb3a8 | ||
|
5b93c2b907 | ||
|
bbd72a79cb | ||
|
babf5d840c | ||
|
90091db3b1 | ||
|
ef03dc12bb | ||
|
a35014f0b5 | ||
|
abb0a049a8 | ||
|
3de18e2a86 | ||
|
399bbd56ec | ||
|
38999d284a | ||
|
b61b63d100 | ||
|
0dc92fbc99 | ||
|
1cb7cd8859 | ||
|
199700cf09 | ||
|
ee78b7898b | ||
|
a47ded7cf2 | ||
|
770f031b8b | ||
|
18139c7114 | ||
|
4a64654f49 | ||
|
377ff279b8 | ||
|
d5d39e1c2a | ||
|
f7b1da7557 | ||
|
5e8c6c6b23 | ||
|
64634ef07b | ||
|
7de2474611 | ||
|
ea2b6a9ad8 | ||
|
6ee7be7cae | ||
|
12690ed464 | ||
|
90361242ac | ||
|
cd8daa929a | ||
|
a07c579497 | ||
|
e78f619be1 | ||
|
5e7ff36e8e | ||
|
e7e0caaa41 | ||
|
00c6e41239 | ||
|
c604ae38dd | ||
|
25e25c2285 | ||
|
b4180b5b48 | ||
|
d14bda7e7a | ||
|
045ddc5ca5 | ||
|
3f3fad0db7 | ||
|
30c77b891d | ||
|
5e126364ed | ||
|
7f2f9588ac | ||
|
e299e246e3 | ||
|
0541a0c69f | ||
|
1178f7c892 | ||
|
a3e78bd76d | ||
|
bcf33b779e | ||
|
d0ba506011 | ||
|
477d1f2aee | ||
|
aa4b1ad8e8 | ||
|
6b902a7ad8 | ||
|
a626f2ce29 | ||
|
2685a02e1a | ||
|
bf1f201d4b | ||
|
335578dd91 | ||
|
3a3fc986c4 | ||
|
0e1e54aff4 | ||
|
0b8f24a92e | ||
|
3cbcd4630c | ||
|
709ab3dd88 | ||
|
b3d93d0761 | ||
|
79d1539149 | ||
|
ba1a7e0fd6 | ||
|
30ebfd84cf | ||
|
aef215d810 | ||
|
cee872e944 | ||
|
c50e8f9de8 | ||
|
11a8e3465f | ||
|
e8ed7c1e3e | ||
|
5b822d238d | ||
|
053754fea8 | ||
|
7a9fd118a3 | ||
|
2aa3c8cfe1 | ||
|
ec2bf37ad4 | ||
|
fc5c959a55 | ||
|
b8cb48aead | ||
|
4eccedcd78 | ||
|
08b1160d63 | ||
|
3bcd8b8b2c | ||
|
4b20b65a22 | ||
|
4b234a3211 | ||
|
2ca7310baf | ||
|
5d742a9037 | ||
|
a3465d292c | ||
|
8a3a094414 | ||
|
52d5f2c992 | ||
|
60e0f972d0 | ||
|
0a9182519d | ||
|
bfded924d7 | ||
|
c653c458b9 | ||
|
4b7ffa1465 | ||
|
a5c871e933 | ||
|
56983c4d2a | ||
|
e9721ecc4d | ||
|
84fe9c84a8 | ||
|
40ff3fd4bf | ||
|
e92074e586 | ||
|
424c1f7d8f | ||
|
868707dbde | ||
|
29d54fe589 | ||
|
c246ae3bcf | ||
|
ab887c656a | ||
|
eefeec7f0a | ||
|
74777df344 | ||
|
c21ce578f5 | ||
|
7d1b7adda5 | ||
|
f64528dd87 | ||
|
3ca1d03d34 | ||
|
49929bd473 | ||
|
25f15e12cf | ||
|
b6d15c27b6 | ||
|
2f8c5c9b20 | ||
|
bee1373f92 | ||
|
8a4535d55e | ||
|
613fb60859 | ||
|
18f9978fcf | ||
|
9b527cb53c | ||
|
c06e4af79d | ||
|
e89c3ed65e | ||
|
eb60cfb083 | ||
|
65ce7429b7 | ||
|
4b4a4927c4 | ||
|
f57d2df5ec | ||
|
d70e18ba8d | ||
|
962aeb1c75 | ||
|
6d7711f3ea | ||
|
ba487611ee | ||
|
6a636444a7 | ||
|
25c766ed94 | ||
|
dbc3723bdc | ||
|
3963c60fc1 | ||
|
b888c7729e | ||
|
a39ece6156 | ||
|
624eb5b085 | ||
|
9ec8c903c5 | ||
|
2396a6b35a | ||
|
1fc54e49d9 | ||
|
30ee7a9e85 | ||
|
b9be5e8c44 | ||
|
ae1c6d4617 | ||
|
7b11e157c4 | ||
|
5783b7b392 | ||
|
507d52bcb9 | ||
|
d45929c604 | ||
|
632c32addb | ||
|
7d2b18759e | ||
|
aa8f6f4ad6 | ||
|
592167ffb7 | ||
|
12a6bfad00 | ||
|
1ea3b956ad | ||
|
1b7dafe743 | ||
|
624eb2a2ba | ||
|
88d98517c8 | ||
|
9528606906 | ||
|
09b44a3af3 | ||
|
dc861eca7d | ||
|
e749fe70e2 | ||
|
d20c082deb | ||
|
c4abc3ade3 | ||
|
ff28aab56e | ||
|
1a6f842492 | ||
|
af0cd4a342 | ||
|
d98b3224cf | ||
|
f2e5f0754c | ||
|
ec1601f317 | ||
|
54116608dc | ||
|
fea6735ea1 | ||
|
2f77a07b3d | ||
|
4cd7ccda57 | ||
|
ef337ffb69 | ||
|
9ff5d8d4e0 | ||
|
199a5eed60 | ||
|
e14ddbe23c | ||
|
3878e66ea4 | ||
|
c14a33444b | ||
|
48f7c15035 | ||
|
32212a46e2 | ||
|
b4025c45da | ||
|
bd589c4422 | ||
|
f85f7758e6 | ||
|
49e36b6e00 | ||
|
df6e245e5c | ||
|
4e4f9af5b7 | ||
|
aa9e63b51a | ||
|
82340582b2 | ||
|
9d045e24db | ||
|
1dcc95d16a | ||
|
78c32d51db | ||
|
545a2c6688 | ||
|
359d9285fe | ||
|
1bd51be99c | ||
|
ee56080af0 | ||
|
18196c4a77 | ||
|
14cd3b99cc | ||
|
ff75fe47ee | ||
|
7e2cc180c3 | ||
|
bef9eea87b | ||
|
9cd1aef1db | ||
|
ab25cf0637 | ||
|
32f37720dd | ||
|
766cf3aeae | ||
|
905353d540 | ||
|
38eb3123be | ||
|
0183edd450 | ||
|
2f640a946e | ||
|
ec6c2adf9b | ||
|
5a1edc5ffb | ||
|
149d482c7d | ||
|
c9f784e4fa | ||
|
9edafa75e7 | ||
|
08baba545d | ||
|
60d0b29236 | ||
|
f5ee4fb5b5 | ||
|
5b7b3be6f9 | ||
|
c933560102 | ||
|
12a80f35fe | ||
|
43f901eeb9 | ||
|
e8440e06a1 | ||
|
19e9e9d032 | ||
|
8ff2d687e4 | ||
|
e616e8f9aa | ||
|
1fbc67fe98 | ||
|
a49e4d8d0c | ||
|
fa4ac00504 | ||
|
dc066edec4 | ||
|
eaed7b2899 | ||
|
123ecc3548 | ||
|
c857fcf035 | ||
|
923f710e53 | ||
|
8d8c2ed262 | ||
|
8558673a5a | ||
|
7372f330a4 | ||
|
07c145c661 | ||
|
f69e1d2a0c | ||
|
0021fc24bb | ||
|
82bc1d5da5 | ||
|
ffc343b471 | ||
|
86e43123b0 | ||
|
b558202441 | ||
|
83947b6725 | ||
|
7e8b65e61f | ||
|
cff0ea425c | ||
|
f6ba6d0310 | ||
|
7b9bf9e552 | ||
|
c91ad1e016 | ||
|
b1444b8635 | ||
|
51a49b94d8 | ||
|
720b46d790 | ||
|
9089c30d57 | ||
|
e2d522803c | ||
|
babcb00621 | ||
|
901a983150 | ||
|
955182b05b | ||
|
f4dfe8eeb4 | ||
|
93904b8278 | ||
|
495580ae2b | ||
|
3051004217 | ||
|
b854b5e1ac | ||
|
cf61a96ef6 | ||
|
1293d25e1b | ||
|
c1a201f358 | ||
|
9ee5390ec7 | ||
|
3f1caa702b | ||
|
24f9573c05 | ||
|
83b25920ea | ||
|
834edcd03e | ||
|
8f3801e3c2 | ||
|
b5f7c58276 | ||
|
66ae293ddd | ||
|
8e78b9e405 | ||
|
9da1759247 | ||
|
704c41c0f2 | ||
|
1f422afe3d | ||
|
709a23cf09 | ||
|
bfa3fa293f | ||
|
d8b878b1bb | ||
|
b2589dbc04 | ||
|
2a4b605794 | ||
|
a3080a3373 | ||
|
b4b9ae5d7f | ||
|
bb9a93e462 | ||
|
e55fbf675e | ||
|
0342477733 | ||
|
e6e2b1f142 | ||
|
95c23a93cd | ||
|
ae784cb985 | ||
|
17765bdba9 | ||
|
45f791b3c8 | ||
|
b0ad39ed0b | ||
|
1988aeb945 | ||
|
f48a5710aa | ||
|
c8d2410c27 | ||
|
a9ef32c32f | ||
|
19b6f78c8a | ||
|
4ab507fd43 | ||
|
2662df2547 | ||
|
0332415ac9 | ||
|
a0f9fb90ee | ||
|
1b8c4cb832 | ||
|
cfd50d281b | ||
|
1667856894 | ||
|
c5c76ab539 | ||
|
eb060d2e43 | ||
|
b5556613cf | ||
|
ad1a057edb | ||
|
8342f985e5 | ||
|
9d32e58915 | ||
|
84027cbae1 | ||
|
5343040ef3 | ||
|
75cd7dd4b9 | ||
|
fb6ffd8e09 | ||
|
aca9e61f39 | ||
|
56078098a4 | ||
|
1f197965e0 | ||
|
183f0f6c42 | ||
|
76624c7d83 | ||
|
ab47c56ec5 | ||
|
ad35ed96c8 | ||
|
97bd51cf5f | ||
|
f4a110cfd7 | ||
|
e9ed06adfd | ||
|
bc5eaf34fe | ||
|
3e8b891dd0 | ||
|
3f2ea4bc64 | ||
|
ae81b37c1d | ||
|
b4349afb1b | ||
|
0cffe8e3f7 | ||
|
7a1f4f9abe | ||
|
5768b01786 | ||
|
7e9079467f | ||
|
615e96922e | ||
|
8b94c452fb | ||
|
66dd510acc | ||
|
96e044566c | ||
|
726ad84d7f | ||
|
4fc422f880 | ||
|
1938024ab3 | ||
|
05b1de6fd5 | ||
|
0bf8807b50 | ||
|
23668ee74e | ||
|
6bc5c5a808 | ||
|
330004dc72 | ||
|
a900d6d742 | ||
|
bfea70eec9 | ||
|
13babbf330 | ||
|
0a6fe382ac | ||
|
dd6dea9dfc | ||
|
11434fba68 | ||
|
10bf90c92a | ||
|
5193fe4bab | ||
|
4df4995623 | ||
|
f5f4ecf46a | ||
|
69b65d06a4 | ||
|
e6bf4e836d | ||
|
01d67eb239 | ||
|
3514cfbd44 | ||
|
cade35fe10 | ||
|
2c6df77f5a | ||
|
0276ae91b5 | ||
|
60fee2accb | ||
|
03e2632294 | ||
|
194fe59458 | ||
|
e400111f8a | ||
|
8bc7607783 | ||
|
4108451ebe | ||
|
0f56257aa1 | ||
|
2a575284b9 | ||
|
444bcbfb30 | ||
|
212021c878 | ||
|
600b687253 | ||
|
ae3aada172 | ||
|
50cc8af8c3 | ||
|
1809cdbf37 | ||
|
fb38047769 | ||
|
08e0d6a34a | ||
|
b4fddaec7f | ||
|
3f19d22941 | ||
|
c7d51cfc4c | ||
|
6829d34e90 | ||
|
021e430103 | ||
|
ae6f42a7fb | ||
|
da98097394 | ||
|
43f0b935cf | ||
|
db5d14e0ce | ||
|
b25cf5eadb | ||
|
bd3b41c809 | ||
|
31d301064a | ||
|
b95b9bdaf3 | ||
|
937f0b5687 | ||
|
ffbc892e44 | ||
|
9e872932d1 | ||
|
ecb2fbf900 | ||
|
72edd2e15d | ||
|
c3d22c554f | ||
|
e72f4a1582 | ||
|
5e43c060fa | ||
|
e8dee1ddd9 | ||
|
d85b8c776f | ||
|
fb518b6218 | ||
|
8d510cde21 | ||
|
da4bf167ee | ||
|
dc1abf8dcb | ||
|
3fc969a50b | ||
|
4064129d49 | ||
|
22b4e0bfff | ||
|
53fa1f88fd | ||
|
5fefcd92f3 | ||
|
d20bc936da | ||
|
e79f036a70 | ||
|
0f2af91a04 | ||
|
7e361cb3f1 | ||
|
1f7acd4027 | ||
|
dddca7bf4b | ||
|
e520be80c0 | ||
|
0b78dedf04 | ||
|
8f409477c8 | ||
|
9c22f7ce69 | ||
|
05fa916915 | ||
|
9a593d7a61 | ||
|
30017eeaa0 | ||
|
9b03cacc94 | ||
|
557970cf54 | ||
|
d90dac60ef | ||
|
b4b2e67260 | ||
|
bcd5e935e7 | ||
|
b322627609 | ||
|
09861095ff | ||
|
47e567fb40 | ||
|
5bb30ad28f | ||
|
a46bbf05ec | ||
|
04a4705dbd | ||
|
3271c96531 | ||
|
05fa6b9aba | ||
|
7edd7f27cb | ||
|
4a6b12eda1 | ||
|
a354e1a0eb | ||
|
c2c419522a | ||
|
7a3cc7d242 | ||
|
bd6f02f6af | ||
|
bd33728fd7 | ||
|
8400ee8a3d | ||
|
80ee7aef8e | ||
|
41762be3a5 | ||
|
605dda7b76 | ||
|
8d6e3b5a58 | ||
|
53920fc368 | ||
|
96d10423d9 | ||
|
42be9b5b51 | ||
|
0add041516 | ||
|
dee76cac8d | ||
|
9646fab22c | ||
|
a54e9cb244 | ||
|
f8339c7a9a | ||
|
9e2c864117 | ||
|
e1ccd71455 | ||
|
0e82426ea1 | ||
|
6c7afd168a | ||
|
e014fdf3da | ||
|
2074da05c8 | ||
|
84cd87b70a | ||
|
75f87044f6 | ||
|
f71b51e64a | ||
|
9d741cdd63 | ||
|
56be17000a | ||
|
f6aacbc31d | ||
|
ad4b57c327 | ||
|
3794b2f0de | ||
|
e0e8412728 | ||
|
05b8175aad | ||
|
339c89dd1c | ||
|
402299e9c8 | ||
|
d0e8f450fd | ||
|
e97b1ccd7a | ||
|
68691b91ef | ||
|
213d417481 | ||
|
2ec841eb61 | ||
|
60b5c54be1 | ||
|
448412a191 | ||
|
e06327936e | ||
|
5f044e1aed | ||
|
d90fb6fc9b | ||
|
1c583d2ea9 | ||
|
ddab131102 | ||
|
3b06cf8a2a | ||
|
c5d7c29f3d | ||
|
26af860eac | ||
|
ec9852eb98 | ||
|
f2b8c6d0ca | ||
|
2cf7a048cf | ||
|
3dbb71a076 | ||
|
ddf6760543 | ||
|
a6fa323f33 | ||
|
2df8f1acdb | ||
|
74a3290abc | ||
|
ab306d21f3 | ||
|
1bd7795851 | ||
|
302c142346 | ||
|
7c254c9b45 | ||
|
b1c458359d | ||
|
2d925c62f5 | ||
|
ef4711987f | ||
|
8910dfaecf | ||
|
e8b2e79c7d | ||
|
333ee1e1f7 | ||
|
ba013bbe96 | ||
|
636e66fe7a | ||
|
372a45e667 | ||
|
ab6b796ce2 | ||
|
71e6c02897 | ||
|
3edeaa7038 | ||
|
f2f75d4015 | ||
|
69adade738 | ||
|
1bd88828e4 | ||
|
977fcbe6cd | ||
|
aa76a5c436 | ||
|
ea5fad428d | ||
|
0e1e27b35a | ||
|
d41f67a156 | ||
|
6f10dec808 | ||
|
9b79da2cdf | ||
|
c11b8519f1 | ||
|
b9e23a0b59 | ||
|
75d41b6fe5 | ||
|
4256590735 | ||
|
462fb05ea8 | ||
|
812dbfd836 | ||
|
cfca837777 | ||
|
ac5fb1948a | ||
|
22d8b73625 | ||
|
edde40c298 | ||
|
50ade92238 | ||
|
b598d00aef | ||
|
97bee17e4e | ||
|
6aa5c79332 | ||
|
7c5ce756f9 | ||
|
43d53a9ffa | ||
|
e6589bee5b | ||
|
9f94cad615 | ||
|
7778d9f773 | ||
|
564bd55147 | ||
|
a7ff806522 | ||
|
ac3b074d66 | ||
|
da949c2604 | ||
|
20a9775dc6 | ||
|
8ec19e058f | ||
|
a5c4549722 | ||
|
c7d302aa16 | ||
|
77ac565ff3 | ||
|
2e6f834a50 | ||
|
5b044cca9e | ||
|
2ab5623809 | ||
|
2d07f0d77c | ||
|
8ba7f9f702 | ||
|
460ac7a86b | ||
|
f93a1ab3eb | ||
|
6b02696aa8 | ||
|
25f20d6a85 | ||
|
8387964bae | ||
|
926278617f | ||
|
1933737a0c | ||
|
b3e5288bde | ||
|
ed6e966b2f | ||
|
9315fe36b6 | ||
|
41fbf5ba36 | ||
|
40db481453 | ||
|
1b5e295744 | ||
|
b42a52f7f6 | ||
|
8268df84d0 | ||
|
eb6fa5f1f1 | ||
|
025c8856ed | ||
|
ed421445e9 | ||
|
0cda07106b | ||
|
f335941b62 | ||
|
f0719f8bde | ||
|
27fe28661c | ||
|
bb13b12de6 | ||
|
1117e28b2e | ||
|
0f48873c25 | ||
|
faf61b4af3 | ||
|
e29ce93d32 | ||
|
38ab68492e | ||
|
53b1576e5f | ||
|
2835e79973 | ||
|
8e4d7e989b | ||
|
603c6b826c | ||
|
51bcc77cb3 | ||
|
a30d07b924 | ||
|
11faa5fe39 | ||
|
ce8383b26f | ||
|
bd8f07c90a | ||
|
fee077bebd | ||
|
558f4d93ba | ||
|
e77d126349 | ||
|
cd8667d6de | ||
|
8a2d06d010 | ||
|
92741fc733 | ||
|
1cff162649 | ||
|
f59be465ea | ||
|
27305317ce | ||
|
8378320e50 | ||
|
86a69952db | ||
|
e212ebfdb9 | ||
|
f235125d48 | ||
|
8efa815eb9 | ||
|
e54d42576b | ||
|
1e18539862 | ||
|
673fdf4721 | ||
|
57d6bfba51 | ||
|
8fa23aecda | ||
|
f9c10533e8 | ||
|
c891c3723e | ||
|
a0625ef2e7 | ||
|
b2774abb54 | ||
|
fa089bbc40 | ||
|
8437d1763f | ||
|
0532b0f9df | ||
|
5f8eb853f0 | ||
|
9125a85382 | ||
|
5b3dc44c72 | ||
|
ee5147f111 | ||
|
f67137fd5e | ||
|
b67db4a896 | ||
|
067018828c | ||
|
e990530ada | ||
|
45f863a72a | ||
|
519febbeb2 | ||
|
854b1a4caf | ||
|
08eb95e73a | ||
|
66f8fbf08c | ||
|
86a30bbb29 | ||
|
966c453c27 | ||
|
c7bcaf0193 | ||
|
0ac91e36a0 | ||
|
6f1299ce9e | ||
|
83ca29d649 | ||
|
d0206044dc | ||
|
d4623e4175 | ||
|
68ef03377d | ||
|
d3f9d3d033 | ||
|
a6ccd04471 | ||
|
0fd8174e77 | ||
|
ebc63f479a | ||
|
570c39c20a | ||
|
1d57257cf5 | ||
|
82d7baee0b | ||
|
1d91e1f1f7 | ||
|
15e8894614 | ||
|
0ae96918e9 | ||
|
e970880059 | ||
|
69ae86aba8 | ||
|
8142e1be49 | ||
|
cbc7612451 | ||
|
09e6a7b7cc | ||
|
082e35668a | ||
|
c9e78c837b | ||
|
2d171db672 | ||
|
a8b11abec8 | ||
|
b08333dccd | ||
|
98bad48971 | ||
|
53375bb536 | ||
|
88798093e1 | ||
|
45a28751af | ||
|
1670c44464 | ||
|
a286ac85dc | ||
|
1606adc0ab | ||
|
b4ce557d92 | ||
|
2ba7fff8d4 | ||
|
98efc93610 | ||
|
2680507eae | ||
|
df72392ad2 | ||
|
a6acb15a00 | ||
|
ba7242a815 | ||
|
99740c1324 | ||
|
c04b20d1fa | ||
|
e127067878 | ||
|
6f03b72368 | ||
|
7bd3ee9340 | ||
|
c94db606e5 | ||
|
4d2474226b | ||
|
1ffc4f538b | ||
|
6702ac957b | ||
|
ba42b2e77d | ||
|
fba18ca40a | ||
|
3a2a7a1476 | ||
|
3c6d6b90bc | ||
|
47dc9ee304 | ||
|
0045bf206c | ||
|
669a1a6953 | ||
|
e28452cc7b | ||
|
5ceac03db1 | ||
|
96215e5950 | ||
|
7a030b9224 | ||
|
e743478a4d | ||
|
bc29c80d44 | ||
|
82b8a474d7 | ||
|
99851b297d | ||
|
e3a500e12c | ||
|
378666e3cc | ||
|
b65ad1ebd7 | ||
|
933a821b5f | ||
|
159337ddbd | ||
|
2e25a4333a | ||
|
64fafb0795 | ||
|
5927e0bb3b | ||
|
e21596db83 | ||
|
8a608c3c3e | ||
|
76f9db8516 | ||
|
90f4809b7c | ||
|
3ff1af9fd6 | ||
|
711dd520f7 | ||
|
99503c836a | ||
|
2c8bc51862 | ||
|
5e7aa15232 | ||
|
d0761869b6 | ||
|
bb99b89228 | ||
|
7b88beb0b5 | ||
|
82c630bd0e | ||
|
bb4f11e1b8 | ||
|
3182d20b7c | ||
|
a6f15e2474 | ||
|
0c3f03680c | ||
|
a66e8dfc9f | ||
|
556232aac4 | ||
|
7fcfaadd91 | ||
|
18d388e1e2 | ||
|
7de6622d74 | ||
|
9af1f90990 | ||
|
15ab43963d | ||
|
6b7465a4b0 | ||
|
d64bdf02de | ||
|
4a2c94993a | ||
|
86c318d86b | ||
|
bffc1ad41d | ||
|
2a685c116f | ||
|
0e271d2924 | ||
|
7127c4d590 | ||
|
a82929996b | ||
|
3f01f52c7b | ||
|
9c94494622 | ||
|
c8a508f35c | ||
|
4ec73ef1c3 | ||
|
3465b79e5b | ||
|
726a0a33ae | ||
|
ba05810d80 | ||
|
10bb53e7de | ||
|
dd8a083546 | ||
|
84131a71c9 | ||
|
4909a11896 | ||
|
4069357b81 | ||
|
e6e87bcc40 | ||
|
7ba22045e8 | ||
|
d4040b61c4 | ||
|
7534f8fc37 | ||
|
7ced009e92 | ||
|
cc13c71e40 | ||
|
d2f09209a7 | ||
|
a241b04d99 | ||
|
4f35d907cb | ||
|
9e50d144a3 | ||
|
8514bb48c4 | ||
|
3ae633a2a1 | ||
|
2799cb9118 | ||
|
d20346b6ac | ||
|
2f00666a68 | ||
|
80f4570093 | ||
|
3f0a2ba48f | ||
|
fc90094e8a | ||
|
20d580ade8 | ||
|
290a024a9e | ||
|
64a283c3a6 | ||
|
3e93f0ecbc | ||
|
b12bcc1c49 | ||
|
05d7313dcc | ||
|
c0b6e549f0 | ||
|
31f9bf219e | ||
|
4906ca7059 | ||
|
b963a7a0e5 | ||
|
5d6dadda76 | ||
|
77f69fd223 | ||
|
7f5052bc87 | ||
|
3009c1f4f6 | ||
|
5425872bba | ||
|
0759e17295 | ||
|
56a59d25df | ||
|
887f92babe | ||
|
197f942b3f | ||
|
a5be7a35e9 | ||
|
e347675265 | ||
|
02a084d571 | ||
|
7fbcc23d94 | ||
|
bda87167a3 | ||
|
34f53c287e | ||
|
9ab07f37d7 | ||
|
36399b39fb | ||
|
1e015287a1 | ||
|
a590aa8485 | ||
|
c0582fde66 | ||
|
d8d75b4afa | ||
|
9d4942723c | ||
|
e5b1ece5c0 | ||
|
1b25bfbf5a | ||
|
7c64d9f42a | ||
|
61f9f4ef58 | ||
|
b8eac56213 | ||
|
ab5fb5749b | ||
|
e0fb9a1b25 | ||
|
97e717f646 | ||
|
caa82ad1e6 | ||
|
7ec251ea6d | ||
|
675b0c3cca | ||
|
05e54d4b7f | ||
|
b4527c90e5 | ||
|
dbc626734d | ||
|
3f2ce3cb80 | ||
|
0820d6d6fb | ||
|
a8c74e39c2 | ||
|
d58c29907d | ||
|
2322733427 | ||
|
d76e8fe85a | ||
|
0c2e4fe34a | ||
|
30b6c87a49 | ||
|
540221467a | ||
|
d44d331a78 | ||
|
595554f18a | ||
|
84d3c3f7eb | ||
|
890f55c9dc | ||
|
575a14e1f3 | ||
|
91074bebd6 | ||
|
3459a16b48 | ||
|
88c9b6849f | ||
|
d87bb25baf | ||
|
34cb796505 | ||
|
1eb34e0662 | ||
|
01d631033f | ||
|
84b2c07340 | ||
|
a86eeb636d | ||
|
417a5a8214 | ||
|
bcf40dde8c | ||
|
cad87484c7 | ||
|
b8ad641ed6 | ||
|
ae8197be8b | ||
|
eafb0274a7 | ||
|
71a23a4e02 | ||
|
26fd90dfa3 | ||
|
e393ebede2 | ||
|
2981bb3f19 | ||
|
6e9f05abb1 | ||
|
9df1115380 | ||
|
f22e360cbb | ||
|
67769af6f4 | ||
|
b1a9a1d6d9 | ||
|
cf0606ecb7 | ||
|
7287edcd6f | ||
|
e0d26203dd | ||
|
7e3a85e9da | ||
|
5a0fed9c93 | ||
|
1f1e743912 | ||
|
b4301ed0d5 | ||
|
b5391560fc | ||
|
718989cbcf | ||
|
d0aee76962 | ||
|
fb08af96bd | ||
|
510a0c5e70 | ||
|
89bdc8ec75 | ||
|
ae25ade318 | ||
|
dd5e46a8a7 | ||
|
f7218aaa9e | ||
|
ee79bcfc44 | ||
|
2c2c174874 | ||
|
f57c49ce3a | ||
|
06121acfac | ||
|
2078ce7446 | ||
|
49f41abfdb | ||
|
70fcfe6d6c | ||
|
b060b4b9bf | ||
|
ee2135bfb3 | ||
|
7daa322441 | ||
|
74252cb66b | ||
|
5fe2be031f | ||
|
2ba3aaba47 | ||
|
ad8903991c | ||
|
3e5624c570 | ||
|
88d3bba300 | ||
|
f5ee6f3537 | ||
|
afc77e7adc | ||
|
024f42fce6 | ||
|
8a5f12b97c | ||
|
954b21cf39 | ||
|
74d095774d | ||
|
17a2722e6d | ||
|
c843bddbfe | ||
|
3f22a49755 | ||
|
7af2ffcebf | ||
|
de90c959e0 | ||
|
9987dc1eb4 | ||
|
3efd575dd2 | ||
|
f4c7b17a87 | ||
|
16d80718cb | ||
|
ad228d53b7 | ||
|
15ee1e531b | ||
|
1c8fb3392a | ||
|
8647866a32 | ||
|
23351c4f1c | ||
|
1367c304cf | ||
|
26d6bcb074 | ||
|
b0d651ece1 | ||
|
b6d50ba6a4 | ||
|
b3ab6a9166 | ||
|
f39a5ac9c2 | ||
|
38a9a9240d | ||
|
241b22a465 | ||
|
741abc0366 | ||
|
7854775de5 | ||
|
e62eaa6b4b | ||
|
b4cce23ef4 | ||
|
2bcaf90fc8 | ||
|
96ffbe2f84 | ||
|
6f5acee1c3 | ||
|
54e491d8bf | ||
|
ab6374e278 | ||
|
2fda4c9f67 | ||
|
5138a45b0f | ||
|
b224d4fa8a | ||
|
a552e44483 | ||
|
0cf3bba118 | ||
|
2c48ea3508 | ||
|
b9b6212b75 | ||
|
b978aaec21 | ||
|
af704681d9 | ||
|
1443ddfe6c | ||
|
54457a3e1b | ||
|
bf180e6a2c | ||
|
864a5820c9 | ||
|
4d3ca49c3f | ||
|
c49c3cf7f0 | ||
|
5d5ab57469 | ||
|
31978d8de0 | ||
|
e8eb68bf24 | ||
|
9ea08f4fed | ||
|
fe078a5c5b | ||
|
61933954f3 | ||
|
4c243638cb | ||
|
02ba04b5d8 | ||
|
4f158a4829 | ||
|
177a22df59 | ||
|
6b0ca2966e | ||
|
aadfaf7150 | ||
|
b307b9a66b | ||
|
6c1ab6002d | ||
|
9638eefc91 | ||
|
9e9c4ad587 | ||
|
ce231431b9 | ||
|
06e1e14e02 | ||
|
416e7884f5 | ||
|
d579222007 | ||
|
30243c84cd | ||
|
3557a77645 | ||
|
97be28638b | ||
|
aba0826c38 | ||
|
f032228d0e | ||
|
6cf174c5ed | ||
|
c2109d245f | ||
|
6a9745171e | ||
|
f9a68e8b23 | ||
|
6e391df5ee | ||
|
f5edca94d3 | ||
|
60046abec3 | ||
|
cafc2060b8 | ||
|
b1f45752cf | ||
|
ed17551170 | ||
|
ef5adab638 | ||
|
fb653ff99d | ||
|
78fc47a9c4 | ||
|
2a124cea61 | ||
|
4c7cc563dc | ||
|
6114af4f93 | ||
|
8c31629655 | ||
|
03c8a8edb2 | ||
|
3eeaee154f | ||
|
8cf8fa7c80 | ||
|
6b4f6fc71e | ||
|
30c2680b6f | ||
|
fb7b1800cc | ||
|
ff573bf377 | ||
|
0a33bb861e | ||
|
728756289b | ||
|
56ccd3a0ac | ||
|
66f3f0ba07 | ||
|
af5e0d589e | ||
|
533dc99e7d | ||
|
fc5ca965ba | ||
|
9c4a46bcdb | ||
|
52658886e7 | ||
|
8174ab7616 | ||
|
2b6acedae1 | ||
|
d00fe9c5f4 | ||
|
88aa270728 | ||
|
4ae409c7f4 | ||
|
9a29c9abdd | ||
|
66d93ea037 | ||
|
6c0066dbfb | ||
|
4fde644733 | ||
|
e7841c60df | ||
|
94f647b24a | ||
|
630249d22a | ||
|
db99b4cb54 | ||
|
c77db23586 | ||
|
daf66bcec4 | ||
|
8caf36349f | ||
|
6934de58e5 | ||
|
54a5007c01 | ||
|
e25a455698 | ||
|
ab429dfeb6 | ||
|
c5289dc0e8 | ||
|
d191877002 | ||
|
4d979160c2 | ||
|
d00e8f6e19 | ||
|
91b77e5237 | ||
|
5b8c246d53 | ||
|
b374b9b91c | ||
|
403717117e | ||
|
027295d995 | ||
|
c9b7eccbc1 | ||
|
2b6d9348cd | ||
|
692f8c8454 | ||
|
6783355c4d | ||
|
fb9cca1e99 | ||
|
eb770ede1a | ||
|
2643e853af | ||
|
b79456e91b | ||
|
ac66c086f8 | ||
|
ebccf401dd | ||
|
66494845b7 | ||
|
e6c36ad602 | ||
|
26379182db | ||
|
bba03d14d4 | ||
|
23b728a762 | ||
|
819c5207d2 | ||
|
311358544e | ||
|
4480ecc96d | ||
|
6c5f70a205 | ||
|
99adfb4a9e | ||
|
7909cf4067 | ||
|
780ab20aeb | ||
|
73119bb7c5 | ||
|
f20f0f064e | ||
|
1b44ae098c | ||
|
453c230716 | ||
|
439d885ee1 | ||
|
43dee3ef76 | ||
|
c71ba6ff8d | ||
|
fb7a36eb73 | ||
|
e7f294a065 | ||
|
d5037f180e | ||
|
e90158809a | ||
|
0cb802ed9a | ||
|
d0b47204f4 | ||
|
351cb391e5 | ||
|
051be927cd | ||
|
8bad2c2113 | ||
|
2bcf6fb3eb | ||
|
d1ba0ed2b2 | ||
|
6e421e60c5 | ||
|
8385050804 | ||
|
bfe4f08232 | ||
|
132f914b0d | ||
|
97d82b03ab | ||
|
f06fa3f9b7 | ||
|
6337788a22 | ||
|
024db4358b | ||
|
173f35487e | ||
|
74cbe82dd1 | ||
|
a8c30d30a9 | ||
|
1b7a52d5af | ||
|
e5109789bf | ||
|
4d2b35e09d | ||
|
5c5177ec57 | ||
|
d93b00cd15 | ||
|
39ade59174 | ||
|
0309c815b9 | ||
|
ce613098db | ||
|
ab34044196 | ||
|
17f724748f | ||
|
f3a917b5e7 | ||
|
376011ea08 | ||
|
2ce7624c14 | ||
|
c62dddd5b9 | ||
|
490ba9f140 | ||
|
f76890cc56 | ||
|
e1c8f1fed9 | ||
|
6e19a4ab8b | ||
|
b156523a7f | ||
|
d20b745ae5 | ||
|
c51e9e6b2c | ||
|
1a31fa9067 | ||
|
558b8499af | ||
|
986c162988 | ||
|
770d5cd11c | ||
|
446d2ab3af | ||
|
2f9bf7f063 | ||
|
72ff9c0964 | ||
|
33b1a11d85 | ||
|
06dabf1e4e | ||
|
28d3691e0b | ||
|
ffa21fbfd2 | ||
|
db028665fd | ||
|
6bc83d9f27 | ||
|
790d832155 | ||
|
f477cecdeb | ||
|
e031389021 | ||
|
e00f826eb8 | ||
|
24f4e8ab99 | ||
|
1550d528bd | ||
|
40c041031e | ||
|
3e555ec9f1 | ||
|
5098f14aab | ||
|
a355a9c85e | ||
|
e7db2a8573 | ||
|
9b18bd0b48 | ||
|
f95ddd594e | ||
|
fe8168784f | ||
|
4046f143f6 | ||
|
e4e16999c8 | ||
|
10f3ba4ff4 | ||
|
3cd2be5081 | ||
|
c9359978f9 | ||
|
781c67b31a | ||
|
020bd129fb | ||
|
8086b6d78c | ||
|
48e14b36b8 | ||
|
b3c1c56579 | ||
|
bd0e932910 | ||
|
525e5f88ae | ||
|
005eeb0e0b | ||
|
d21bb015e8 | ||
|
7338838b0e | ||
|
8bb4803ff9 | ||
|
892b919cf3 | ||
|
572d81ecef | ||
|
a4ce7a4037 | ||
|
6eafb15cf9 | ||
|
e19fe5ce1c | ||
|
9d2017e598 | ||
|
f33c419ed9 | ||
|
f425fbaf9d | ||
|
bcc5ec897a | ||
|
f5f2fe3472 | ||
|
b7c3863882 | ||
|
d759f48ee8 | ||
|
3a37075e71 | ||
|
58366c0b87 | ||
|
2667ed13f1 | ||
|
34daffbc96 | ||
|
be81cd72af | ||
|
4ae00714d2 | ||
|
f24cbb6692 | ||
|
5a35c5b928 | ||
|
1880da6351 | ||
|
df93cb103c | ||
|
63b474a32c | ||
|
abdbf158d1 | ||
|
ee68d80d0a | ||
|
c8e6f3a627 | ||
|
dc941575fe | ||
|
e64103e5f2 | ||
|
f0ab03a9fb | ||
|
09965f1cc6 | ||
|
d2852bb34a | ||
|
b03c52a501 | ||
|
fd6633f536 | ||
|
320ac81f48 | ||
|
70bcd2428f | ||
|
71f5ada0a3 | ||
|
aab5141404 | ||
|
ff6b119f27 | ||
|
7ef4b7eeb8 | ||
|
4668be6e24 | ||
|
a211fc7c97 | ||
|
54af0b675d | ||
|
8c8171e774 | ||
|
0bb1790206 | ||
|
45fc84d8be | ||
|
f7500f4cad | ||
|
0cfd718d8a | ||
|
5f486d0f51 | ||
|
6e5a2f85a1 | ||
|
e66a89208d | ||
|
7f65c501c6 | ||
|
22c2382765 | ||
|
38e1a4febf | ||
|
8db554b377 | ||
|
5f5cc55331 | ||
|
2f8b39186f | ||
|
a3a724e2e6 | ||
|
73235c8699 | ||
|
7e5999e862 | ||
|
afa244dcb0 | ||
|
4ea5bb2390 | ||
|
e545d552f6 | ||
|
56fe7b3596 | ||
|
eb28ec866a | ||
|
4649454282 | ||
|
a45dc6efda | ||
|
9e56b9fbb5 | ||
|
5504994cb9 | ||
|
acff6d0432 | ||
|
eea9cb7c5b | ||
|
c0ddddfb1f | ||
|
f7dab3ca56 | ||
|
e11b4038a3 | ||
|
b635e64803 | ||
|
20a194b49a | ||
|
33a5fb8837 | ||
|
59dae640db | ||
|
a6ac75e97b | ||
|
cc5b33a8ec | ||
|
6e1a627b84 | ||
|
62d4c65e1c | ||
|
600134a3ac | ||
|
df14af7337 | ||
|
2f740b570d | ||
|
294d980b52 | ||
|
c9d9909d74 | ||
|
90eeabae7b | ||
|
a32c77c5f1 | ||
|
910846f2ce | ||
|
35d0438261 | ||
|
515af472ce | ||
|
f062f7f9fe | ||
|
f68378041f | ||
|
30ef3057ac | ||
|
48f3a934c9 | ||
|
38d1fde84f | ||
|
dd78670a4b | ||
|
b8a8190a43 | ||
|
2d0989342f | ||
|
15ff9b06a1 | ||
|
41b19f69de | ||
|
f2bd6f1fce | ||
|
ec94218a4b | ||
|
c2e5c7cf38 | ||
|
bac5734527 | ||
|
bef4033d94 | ||
|
64216cba67 | ||
|
a9d6167a9f | ||
|
6c6b40548f | ||
|
a916d88e85 | ||
|
ab2f0548a3 | ||
|
e30ba07285 | ||
|
1b336d973d | ||
|
7b1866737f | ||
|
eedc4ab648 | ||
|
5e429c7a94 | ||
|
57d23eb043 | ||
|
5ebd9be89a | ||
|
5a743779e2 | ||
|
0495062110 | ||
|
d522191f69 | ||
|
bbbdc8b7a6 | ||
|
c00ed8aa5a | ||
|
0beaa94068 | ||
|
96e76665d6 | ||
|
6423a29600 | ||
|
8bddf30dcf | ||
|
a6b2db182d | ||
|
b9f3f9d859 | ||
|
6331447dcd | ||
|
4213ee660f | ||
|
3bd9287c5d | ||
|
b23d3f7c8b | ||
|
f8d9e5e027 | ||
|
8fa7fb3b1f | ||
|
63d92a0872 | ||
|
509d83365e | ||
|
d6ab73c905 | ||
|
5a93ba05d5 | ||
|
2d2fdf0b1e | ||
|
cf96622261 | ||
|
47c8ccb0e5 | ||
|
02fdb8778b | ||
|
6bed7f0e66 | ||
|
7597d335b9 | ||
|
f32c5ba244 | ||
|
ab53ab21e2 | ||
|
acfe5dbb49 | ||
|
f9b91fa189 | ||
|
2462e68ba1 | ||
|
019b2a1681 | ||
|
e94a07b677 | ||
|
c058c29755 | ||
|
9e7c7ac163 | ||
|
fcee41f00d | ||
|
ff2c1bf8ed | ||
|
a2cf26e7ed | ||
|
71e6900375 | ||
|
3bdc98bd12 | ||
|
68d2a6e951 | ||
|
acf47ac947 | ||
|
fda24e5f5a | ||
|
05eca8e4d8 | ||
|
b915f3b1a9 | ||
|
ab58968f4d | ||
|
820d1e7570 | ||
|
a120c8cf98 | ||
|
5a35ea116f | ||
|
e72ad1f030 | ||
|
a17362437a | ||
|
8643dc43b1 | ||
|
e1c869a358 | ||
|
2f5b8d9abe | ||
|
db324f54eb | ||
|
3242efb1a2 | ||
|
30f9f1a021 | ||
|
3f58d05aa7 | ||
|
c611a51575 | ||
|
73458dcd28 | ||
|
23ebeb1cc0 | ||
|
a7ba9f1886 | ||
|
d5192e2244 | ||
|
7eb595170f | ||
|
bd576ca808 | ||
|
2f8146b11f | ||
|
7f321c89cb | ||
|
fa65f606b8 | ||
|
7a3285adaf | ||
|
8bf78fef10 | ||
|
78f97ce4df | ||
|
66ccbf70f8 | ||
|
cb48600b40 | ||
|
33ce235713 | ||
|
f1a68ece01 | ||
|
dd563360af | ||
|
7f6dac4271 | ||
|
178fd8e828 | ||
|
1641e02a7d | ||
|
cfd04e9bb4 | ||
|
38effaf740 | ||
|
a68eb4a73e | ||
|
c02990ef98 | ||
|
5aa7d5ffe9 | ||
|
4942b0fca5 | ||
|
aed1bdff5a | ||
|
929c840006 | ||
|
57aef1d3c2 | ||
|
c1734a094c | ||
|
99c46685ac | ||
|
066b08040a | ||
|
35de2334fb | ||
|
5564c966a5 | ||
|
0891b5e7b7 | ||
|
f3341f201f | ||
|
bf0dafabe2 | ||
|
60f6587169 | ||
|
a9bf32919e | ||
|
7633f9f88b | ||
|
9b56233938 | ||
|
65074264b8 | ||
|
4f6fceb87f | ||
|
659fdd1d37 | ||
|
8bce2ce040 | ||
|
55cc83a5d4 | ||
|
6a51e93ded | ||
|
fcf570e96e | ||
|
cbc184e953 | ||
|
98f9063352 | ||
|
c278f1af00 | ||
|
fbb17a0ba5 | ||
|
8637a9823e | ||
|
d717fe7e8c | ||
|
5e8dfe7267 | ||
|
aaa4fbcdbd | ||
|
150f0cf486 | ||
|
711b220a05 | ||
|
b615c485f7 | ||
|
2f3b6b29ae | ||
|
7aecd83c4a | ||
|
3b460ab91f | ||
|
661c0eb970 | ||
|
f4a1f1fdc8 | ||
|
b5432c3728 | ||
|
6d0625c409 | ||
|
2f1ea08b8a | ||
|
a092da1943 | ||
|
43ef3cc562 | ||
|
047774475c | ||
|
91b8504df5 | ||
|
a4fb2dfcf8 | ||
|
ba16ec02e5 | ||
|
55b7af2623 | ||
|
b428eff10e | ||
|
35aee15b6d | ||
|
6f53c1bfde | ||
|
db0230ed75 | ||
|
7fa3c1e12a | ||
|
3b5993652f | ||
|
be4ad8947f | ||
|
2ead15e78e | ||
|
aa0740ff94 | ||
|
6d3f837820 | ||
|
359b82c29c | ||
|
b54171bc2c | ||
|
936b2131e0 | ||
|
aaef6a9e9c | ||
|
9327006e61 | ||
|
5225c841ae | ||
|
8464e319fd | ||
|
98b8bd90ea | ||
|
ae94993b09 | ||
|
72f10aaed1 | ||
|
4a74bae8c7 | ||
|
5164aec37b | ||
|
1581e9b1cd | ||
|
536e3ffb11 | ||
|
d6103191ba | ||
|
49e37a19a5 | ||
|
d02431e260 | ||
|
f24f6ead92 | ||
|
b7bdb7ae50 | ||
|
2dcf8ac96f | ||
|
5421c94853 | ||
|
10f6bc092a | ||
|
be9ea4ea8e | ||
|
f5c6fa842a | ||
|
e0943ce905 | ||
|
61e7df77a7 | ||
|
a5434360bc | ||
|
ba29c66e3b | ||
|
b3a72d55ae | ||
|
c382ad1cc8 | ||
|
363e28f323 | ||
|
d695656b8c | ||
|
31c6cbc0a2 | ||
|
b93284bc2f | ||
|
99c855b01b | ||
|
c2eaedc959 | ||
|
a631cd67ec | ||
|
66fccd3c68 | ||
|
9a4ebf4daa | ||
|
91c14211c6 | ||
|
1ea07c458b | ||
|
591add6e0c | ||
|
b99fff66df | ||
|
f54ba05b00 | ||
|
6596fb00c7 | ||
|
df0b5a80dc | ||
|
8cdbb31dbe | ||
|
0be4b1222d | ||
|
18a6bfd73a | ||
|
5e2271b237 | ||
|
798999d490 | ||
|
0e68c5e8bc | ||
|
9694af82f4 | ||
|
28a4386975 | ||
|
a238f7beba | ||
|
75ba16281b | ||
|
ad65a278d4 | ||
|
a2320b3f8d | ||
|
ae12853ad0 | ||
|
68adf6dc2f | ||
|
f88989bd4b | ||
|
77ef3847ce | ||
|
b8291837fc | ||
|
423a7f951a | ||
|
557f5fe364 | ||
|
5308970ad8 | ||
|
6b368953f4 | ||
|
2d0b63c29d | ||
|
34e941c8cb | ||
|
76cf007fff | ||
|
321963be83 | ||
|
c733bda6c3 | ||
|
e67b50b976 | ||
|
9e3c080909 | ||
|
cb661aaebd | ||
|
573571978c | ||
|
dc492a2a0a | ||
|
e47747dd0e | ||
|
4de944b41e | ||
|
5f7a1fa5cd | ||
|
b8ad328cde | ||
|
3076fd19c1 | ||
|
fac437b8c1 | ||
|
697c25015e | ||
|
d83b404fc3 | ||
|
ab7313cbc4 | ||
|
1b8d70322b | ||
|
844ffab4ed | ||
|
f5d0513d1f | ||
|
557abd4285 | ||
|
f4f1a0fbc6 | ||
|
74e10dc012 | ||
|
359812b7ed | ||
|
c2449e53c4 | ||
|
1a91249da2 | ||
|
b9162f9576 | ||
|
f726e8a7b3 | ||
|
2f284cfdc9 | ||
|
b74696adba | ||
|
c8ccba0192 | ||
|
41b0e05f62 | ||
|
847d5f1b3b | ||
|
0445f9dfc2 | ||
|
3001c780bd | ||
|
68ad5e2320 | ||
|
b5e229b19c | ||
|
453d798fcf | ||
|
451afdb660 | ||
|
d298bda92c | ||
|
fd99ba6255 | ||
|
6ade0b2b1a | ||
|
9902003da9 | ||
|
0ff2fcac11 | ||
|
e80be8e7b6 | ||
|
fe0ad0f5cb | ||
|
cb8e162f4e | ||
|
ae8c30fe57 | ||
|
ab82db9e22 | ||
|
cb9d3098de | ||
|
c927419c99 | ||
|
c009026961 | ||
|
51cf442fa5 | ||
|
954e3553ee | ||
|
cde17385b4 | ||
|
5b9e078061 | ||
|
0290d9ddfc | ||
|
eab671d102 | ||
|
f9a96ff914 | ||
|
3ec2b46d28 | ||
|
aec30b89e0 | ||
|
309c33e190 | ||
|
9c0276f97b | ||
|
0a3402ff43 | ||
|
bd82308024 | ||
|
db3b634e62 | ||
|
40bcb22977 | ||
|
4bd94092f1 | ||
|
cac1ce6895 | ||
|
5c3696123a | ||
|
4140fc86ee | ||
|
2ad771e7fd | ||
|
5a38468144 | ||
|
0a9c81772f | ||
|
c9649ac501 | ||
|
6ea567742b | ||
|
f31349eaa0 | ||
|
9d44668d5f | ||
|
a12f58c1c7 | ||
|
b5283eaaed | ||
|
4b6189af5f | ||
|
5d0ffbe453 | ||
|
502706931e | ||
|
57c411288f | ||
|
b09b7b11a1 | ||
|
1a7b3c7294 | ||
|
4678fc7dde | ||
|
9f492fad49 | ||
|
bd0a959e18 | ||
|
dd605e2610 | ||
|
9910f6b817 | ||
|
991897aff4 | ||
|
3cde019208 | ||
|
2f6efbed63 | ||
|
5cff4e299b | ||
|
bce33639da | ||
|
366c465cad | ||
|
acc6e05bdc | ||
|
4ce15577cd | ||
|
98fa4d5e65 | ||
|
7fe94076c7 | ||
|
b8b0c8fa63 | ||
|
e585e87bec | ||
|
6a4924bb16 | ||
|
ab0d516d91 | ||
|
b756ae39d0 | ||
|
c15c38ea8f | ||
|
743963318f | ||
|
ed3fc50858 | ||
|
5b886fe6be | ||
|
7074bbc405 | ||
|
ef314c1707 | ||
|
a3e7626dd9 | ||
|
5cb5d74eed | ||
|
6eee10d46d | ||
|
2d95f2b0d6 | ||
|
ec41bb9c70 | ||
|
2d1780b1cf | ||
|
d9ce99887c | ||
|
9b9cc90414 | ||
|
c1eb539a5c | ||
|
e1793f57ef | ||
|
dbd09a8743 | ||
|
0d189ca617 | ||
|
dc9bcc40ee | ||
|
4991f7ff39 | ||
|
a393b8b122 | ||
|
c73b354386 | ||
|
392ecee3ff | ||
|
bae721c49e | ||
|
4e806e21a6 | ||
|
ec0fdf83b2 | ||
|
cb94d8414f | ||
|
8890051c17 | ||
|
cf00c9476f | ||
|
b2a24c7abd | ||
|
732a1f4694 | ||
|
4c5aa7084e | ||
|
fe1fea671c | ||
|
04c754c0ac | ||
|
754c22c84e | ||
|
629331870b | ||
|
78774315cb | ||
|
36b9c07928 | ||
|
40a818630d | ||
|
568511a4cf | ||
|
109fb39e09 | ||
|
68450d2042 | ||
|
8a052bbed6 | ||
|
3afbb56640 | ||
|
c0ad84a491 | ||
|
c72f17605c | ||
|
42fbee0cdb | ||
|
e9b7ec735f | ||
|
743788135f | ||
|
8ea3e6fa26 | ||
|
f23c83e681 | ||
|
b615bda17e | ||
|
f7c7cd1d3c | ||
|
c7e7be4379 | ||
|
d63d49f246 | ||
|
dad94edb20 | ||
|
7108d5f3ab | ||
|
ef47a74920 | ||
|
a43dab8057 | ||
|
f44039b628 | ||
|
08fa5205b0 | ||
|
b91daebd92 | ||
|
9cd6c5c624 | ||
|
650e017b72 | ||
|
18f9d6dec5 | ||
|
4df6571ad9 | ||
|
0f5923a10a | ||
|
bcdae1169e | ||
|
f260d5df49 | ||
|
808b861dd1 | ||
|
17f1c487a8 | ||
|
8dc2c1a38f | ||
|
220a494692 | ||
|
db6bc10196 | ||
|
1880363aeb | ||
|
19c7b59883 | ||
|
749df89229 | ||
|
444f2172fa | ||
|
77a77c0ea7 | ||
|
dbf380a0d1 | ||
|
ade34f5217 | ||
|
e89607799a | ||
|
d05d8d6a9e | ||
|
b6aa50d3dc | ||
|
9df361935f | ||
|
98b8a122b6 | ||
|
c7232522ee | ||
|
5280f1d745 | ||
|
b52a081e7b | ||
|
f981a44861 | ||
|
d7c5ce0750 | ||
|
9ccc66ca1e | ||
|
8606af3616 | ||
|
f6e821ba6b | ||
|
e8dbcf819b | ||
|
bbe2ef4e8e | ||
|
dd15455031 | ||
|
12ac7bb338 | ||
|
46ef348f0d | ||
|
1a55cca8af | ||
|
81ee989f1f | ||
|
c9c06f8a3d | ||
|
72127979c3 | ||
|
1ad3ddef94 | ||
|
97ec5eabf7 | ||
|
e12e3a3f2d | ||
|
4ff625f23b | ||
|
e38dcd85ac | ||
|
0245baf1b6 | ||
|
10b55c043c | ||
|
457655b416 | ||
|
7e4506c860 | ||
|
794d376348 | ||
|
c60578f5b5 | ||
|
3a9a392a77 | ||
|
a13d4698be | ||
|
c046a1993e | ||
|
f709117cc4 | ||
|
30dd298fca | ||
|
0ff8bb8090 | ||
|
74bbd3c3a2 | ||
|
2e82eaf59a | ||
|
7f1df1f1bd | ||
|
3c79238a44 | ||
|
0aa2565df3 | ||
|
22b11db16e | ||
|
d0e678b5e9 | ||
|
e7bba968b3 | ||
|
4934a24293 | ||
|
ccb68bcda9 | ||
|
66bf4ba3ad | ||
|
78a0cfd052 | ||
|
8548373742 | ||
|
2dfd725ee0 | ||
|
5b779b4f14 | ||
|
fc48aa7155 | ||
|
6193a842f4 | ||
|
eb86b471fe | ||
|
2b52584547 | ||
|
6e3cc57f48 | ||
|
f2c04621a5 | ||
|
3ed6938d4a | ||
|
99fec25ed5 | ||
|
73758ad1fd | ||
|
c53fe0ed1f | ||
|
6082c2bcac | ||
|
069abed784 | ||
|
7d8fa4d78a | ||
|
76081f8d89 | ||
|
5ef4285558 | ||
|
aa7df4282e | ||
|
0ef1d5d0de | ||
|
cceb4bb81f | ||
|
f0f45e007d | ||
|
6a8ffe1642 | ||
|
3a73868c10 | ||
|
ab1b5897a6 | ||
|
f94734a5c8 | ||
|
61b86c9584 | ||
|
1ac1d6e903 | ||
|
b6c58f74c0 | ||
|
c88e99d87c | ||
|
8d7ab9d05e | ||
|
47155a4a29 | ||
|
d0b87fd7c3 | ||
|
d49fd37656 | ||
|
0bd29d71be | ||
|
4b5b62c8ae | ||
|
65fb2ca2d5 | ||
|
0d5bfc0997 | ||
|
4f02c373c2 | ||
|
209a5b1207 | ||
|
95349eacab | ||
|
82ba604b99 | ||
|
46a8dec655 | ||
|
b5af234524 | ||
|
b5c41750f7 | ||
|
6083824eec | ||
|
40977785c3 | ||
|
5eddf4f1aa | ||
|
99a8e25411 | ||
|
08587d8f2f | ||
|
3480d50f61 | ||
|
43af55252d | ||
|
9c43b31fc0 | ||
|
9ec7184aa1 | ||
|
4e2cb30db7 | ||
|
9ca83d3291 | ||
|
6b2172d873 | ||
|
1a5d9f7dad | ||
|
a8425862f0 | ||
|
a3a3db6abb | ||
|
d6c3bc57c0 | ||
|
59c09f90f9 | ||
|
d982b83e14 | ||
|
cc0e930d34 | ||
|
3c3d77fbea | ||
|
da7453fdbf | ||
|
5fcd25506e | ||
|
4979a472de | ||
|
4e8d4f4591 | ||
|
0f5d2b15e0 | ||
|
7fc9631434 | ||
|
df5953dd7b | ||
|
8f5b2b4a0e | ||
|
43c63ffa70 | ||
|
6779bc7459 | ||
|
664be2d0ba | ||
|
6113898b69 | ||
|
79aad6b5c2 | ||
|
6da7757d36 | ||
|
dbb3cb8cc8 | ||
|
579f36a1dd | ||
|
72c2b306cf | ||
|
c2673cd396 | ||
|
8eb152816a | ||
|
a0bc8a21a5 | ||
|
08e4d72758 | ||
|
66340a27fa | ||
|
83fe9835b6 | ||
|
4c1a50a3ca | ||
|
fe44a7b3bc | ||
|
e86d192db7 | ||
|
ea8f1c52f9 | ||
|
a4c1573c45 | ||
|
182bf49ad0 | ||
|
13e1ddbccd | ||
|
327b9a1757 | ||
|
18c48db7f7 | ||
|
b6543bd87f | ||
|
9ad8f71d7c | ||
|
e369311fc2 | ||
|
72ff261fe3 | ||
|
774c6f7e05 | ||
|
771af6ae08 | ||
|
b3cd207444 | ||
|
03f9fa4bc2 | ||
|
e32bfd9aab | ||
|
7e47f8f893 | ||
|
cb816e9653 | ||
|
6b3e7cbc08 | ||
|
db4993ae9b | ||
|
4dc3cf6c6b | ||
|
2b84bbf3a8 | ||
|
26ef4c9961 | ||
|
4f56c38599 | ||
|
240f4dcfb1 | ||
|
ac6abd81c9 | ||
|
14bda4f3a5 | ||
|
61b9670b45 | ||
|
01e8db317e | ||
|
92fc09493e | ||
|
d927209db7 | ||
|
e94007b21f | ||
|
18750f275a | ||
|
d686a853f4 |
15
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
15
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -2,7 +2,6 @@ name: 问题反馈 / Bug report
|
||||
title: "[BUG] "
|
||||
description: 反馈你遇到的问题 / Report the issue you are experiencing
|
||||
labels: ["bug"]
|
||||
type: "Bug"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
@ -12,16 +11,14 @@ body:
|
||||
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
|
||||
5. 请 **务必** 尝试 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本,确定问题是否仍然存在
|
||||
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
|
||||
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
|
||||
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
4. Please be sure to check out [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version update log
|
||||
5. Please be sure to try the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version to ensure that the problem still exists
|
||||
6. Please describe the problem in detail according to the template specification and try to update the Alpha version, otherwise the issue will be closed
|
||||
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the problem still exists
|
||||
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
@ -59,7 +56,7 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to debug, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
|
||||
label: 日志 / Log
|
||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到trace,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to trace, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
|
||||
validations:
|
||||
required: true
|
||||
|
12
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
12
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -2,7 +2,6 @@ name: 功能请求 / Feature request
|
||||
title: "[Feature] "
|
||||
description: 提出你的功能请求 / Propose your feature request
|
||||
labels: ["enhancement"]
|
||||
type: "Feature"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
@ -34,14 +33,3 @@ body:
|
||||
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: os-labels
|
||||
attributes:
|
||||
label: 适用系统 / Target OS
|
||||
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
|
||||
options:
|
||||
- label: windows
|
||||
- label: macos
|
||||
- label: linux
|
||||
validations:
|
||||
required: true
|
||||
|
318
.github/workflows/alpha.yml
vendored
318
.github/workflows/alpha.yml
vendored
@ -2,9 +2,9 @@ name: Alpha Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# UTC+8 0,6,12,18
|
||||
- cron: "0 16,22,4,10 * * *"
|
||||
push:
|
||||
branches: [main]
|
||||
tags-ignore: [updater, alpha]
|
||||
permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
@ -15,201 +15,7 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
check_commit:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version changed or src changed
|
||||
id: check
|
||||
run: |
|
||||
# For manual workflow_dispatch, always run
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Store current version from package.json
|
||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
# Get the previous commit's package.json version
|
||||
git checkout HEAD~1 package.json
|
||||
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Previous version: $PREVIOUS_VERSION"
|
||||
|
||||
# Reset back to current commit
|
||||
git checkout HEAD package.json
|
||||
|
||||
# Check if version changed
|
||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if src or src-tauri directories changed
|
||||
CURRENT_SRC_HASH=$(git rev-parse HEAD:src)
|
||||
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
|
||||
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
|
||||
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
|
||||
|
||||
echo "Current src hash: $CURRENT_SRC_HASH"
|
||||
echo "Previous src hash: $PREVIOUS_SRC_HASH"
|
||||
echo "Current tauri hash: $CURRENT_TAURI_HASH"
|
||||
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
|
||||
|
||||
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
|
||||
echo "Source directories changed"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version and source directories unchanged"
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
delete_old_assets:
|
||||
needs: check_commit
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete Old Alpha Release Assets
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const releaseTag = 'alpha';
|
||||
|
||||
try {
|
||||
// Get the release by tag name
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag: releaseTag
|
||||
});
|
||||
|
||||
console.log(`Found release with ID: ${release.id}`);
|
||||
|
||||
// Delete each asset
|
||||
if (release.assets && release.assets.length > 0) {
|
||||
console.log(`Deleting ${release.assets.length} assets`);
|
||||
|
||||
for (const asset of release.assets) {
|
||||
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
asset_id: asset.id
|
||||
});
|
||||
}
|
||||
|
||||
console.log('All assets deleted successfully');
|
||||
} else {
|
||||
console.log('No assets found to delete');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log('Release not found, nothing to delete');
|
||||
} else {
|
||||
console.error('Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
runs-on: ubuntu-latest
|
||||
needs: delete_old_assets
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch Alpha update logs
|
||||
id: fetch_alpha_logs
|
||||
run: |
|
||||
# Check if UPDATELOG.md exists
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
# Extract the section starting with ## and containing -alpha until the next ## or end of file
|
||||
# ALPHA_LOGS=$(awk '/^## .*-alpha/{flag=1; print; next} /^## /{flag=0} flag' UPDATELOG.md)
|
||||
ALPHA_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
|
||||
if [ -n "$ALPHA_LOGS" ]; then
|
||||
echo "Found alpha update logs"
|
||||
echo "ALPHA_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$ALPHA_LOGS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No alpha sections found in UPDATELOG.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
# 检查 ALPHA_LOGS 是否存在,如果不存在则使用默认消息
|
||||
if [ -z "$ALPHA_LOGS" ]; then
|
||||
echo "No alpha logs found, using default message"
|
||||
ALPHA_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found alpha logs"
|
||||
fi
|
||||
|
||||
# 生成 release.txt 文件
|
||||
cat > release.txt << EOF
|
||||
$ALPHA_LOGS
|
||||
|
||||
## 我应该下载哪个版本?
|
||||
|
||||
### MacOS
|
||||
- MacOS intel芯片: x64.dmg
|
||||
- MacOS apple M芯片: aarch64.dmg
|
||||
|
||||
### Linux
|
||||
- Linux 64位: amd64.deb/amd64.rpm
|
||||
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
||||
- Linux armv7架构: armhf.deb/armhfp.rpm
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- 64位: x64-setup.exe
|
||||
- arm64架构: arm64-setup.exe
|
||||
#### 便携版问题很多不再提供
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- 64位: x64_fixed_webview2-setup.exe
|
||||
- arm64架构: arm64_fixed_webview2-setup.exe
|
||||
|
||||
### FAQ
|
||||
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### 稳定机场VPN推荐
|
||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body_path: release.txt
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
||||
alpha:
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -247,12 +53,12 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@ -264,9 +70,6 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@ -289,8 +92,13 @@ jobs:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: pnpm portable ${{ matrix.target }} --alpha
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
alpha-for-linux-arm:
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -321,7 +129,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: '20'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@ -333,10 +141,7 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: "Setup for linux"
|
||||
- name: 'Setup for linux'
|
||||
run: |-
|
||||
sudo ls -lR /etc/apt/
|
||||
|
||||
@ -359,21 +164,20 @@ jobs:
|
||||
sudo apt update
|
||||
|
||||
sudo apt install -y \
|
||||
libxslt1.1:${{ matrix.arch }} \
|
||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||
libssl-dev:${{ matrix.arch }} \
|
||||
patchelf:${{ matrix.arch }} \
|
||||
librsvg2-dev:${{ matrix.arch }}
|
||||
|
||||
- name: "Install aarch64 tools"
|
||||
- name: 'Install aarch64 tools'
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt install -y \
|
||||
gcc-aarch64-linux-gnu \
|
||||
g++-aarch64-linux-gnu
|
||||
|
||||
- name: "Install armv7 tools"
|
||||
- name: 'Install armv7 tools'
|
||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||
run: |
|
||||
sudo apt install -y \
|
||||
@ -392,7 +196,7 @@ jobs:
|
||||
fi
|
||||
pnpm build --target ${{ matrix.target }}
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
@ -408,6 +212,7 @@ jobs:
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body: "More new features are now supported."
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
@ -415,7 +220,6 @@ jobs:
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
|
||||
alpha-for-fixed-webview2:
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -444,7 +248,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@ -456,9 +260,6 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
|
||||
@ -480,29 +281,16 @@ jobs:
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe'
|
||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip'
|
||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig'
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body: "More new features are now supported."
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
@ -511,3 +299,65 @@ jobs:
|
||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
runs-on: ubuntu-latest
|
||||
needs: [alpha, alpha-for-linux-arm, alpha-for-fixed-webview2]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Update Tag
|
||||
uses: richardsimko/update-tag@v1
|
||||
with:
|
||||
tag_name: alpha
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: |
|
||||
cat > release.txt << 'EOF'
|
||||
## 我应该下载哪个版本?
|
||||
|
||||
### MacOS
|
||||
- MacOS intel芯片: x64.dmg
|
||||
- MacOS apple M芯片: aarch64.dmg
|
||||
|
||||
### Linux
|
||||
- Linux 64位: amd64.deb/amd64.rpm
|
||||
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
||||
- Linux armv7架构: armhf.deb/armhfp.rpm
|
||||
|
||||
### Windows (Win7 用户请查看下面FAQ中的解决方案)
|
||||
#### 正常版本(推荐)
|
||||
- 64位: x64-setup.exe
|
||||
- arm64架构: arm64-setup.exe
|
||||
#### 便携版问题很多不再提供
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- 64位: x64_fixed_webview2-setup.exe
|
||||
- arm64架构: arm64_fixed_webview2-setup.exe
|
||||
|
||||
### FAQ
|
||||
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### 稳定机场VPN推荐
|
||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body_path: release.txt
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
66
.github/workflows/release.yml
vendored
66
.github/workflows/release.yml
vendored
@ -6,10 +6,6 @@ permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
concurrency:
|
||||
# only allow per workflow per commit (and not pr) to run at a time
|
||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@ -44,18 +40,17 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@ -72,8 +67,8 @@ jobs:
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
@ -87,6 +82,12 @@ jobs:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: pnpm portable ${{ matrix.target }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-for-linux-arm:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@ -118,7 +119,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: '20'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@ -130,7 +131,7 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: "Setup for linux"
|
||||
- name: 'Setup for linux'
|
||||
run: |-
|
||||
sudo ls -lR /etc/apt/
|
||||
|
||||
@ -153,21 +154,20 @@ jobs:
|
||||
sudo apt update
|
||||
|
||||
sudo apt install -y \
|
||||
libxslt1.1:${{ matrix.arch }} \
|
||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||
libssl-dev:${{ matrix.arch }} \
|
||||
patchelf:${{ matrix.arch }} \
|
||||
librsvg2-dev:${{ matrix.arch }}
|
||||
|
||||
- name: "Install aarch64 tools"
|
||||
- name: 'Install aarch64 tools'
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt install -y \
|
||||
gcc-aarch64-linux-gnu \
|
||||
g++-aarch64-linux-gnu
|
||||
|
||||
- name: "Install armv7 tools"
|
||||
- name: 'Install armv7 tools'
|
||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||
run: |
|
||||
sudo apt install -y \
|
||||
@ -186,7 +186,7 @@ jobs:
|
||||
fi
|
||||
pnpm build --target ${{ matrix.target }}
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
@ -195,7 +195,6 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
@ -216,6 +215,9 @@ jobs:
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
arch: x64
|
||||
- os: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
arch: x86
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
arch: arm64
|
||||
@ -231,13 +233,11 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@ -262,31 +262,17 @@ jobs:
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe'
|
||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip'
|
||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig'
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
@ -312,7 +298,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@ -337,7 +323,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
4
.github/workflows/updater.yml
vendored
4
.github/workflows/updater.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@ -36,7 +36,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
@ -1,16 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
#pnpm pretty-quick --staged
|
||||
|
||||
# 运行 clippy fmt
|
||||
cargo fmt --manifest-path ./src-tauri/Cargo.toml
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "rustfmt failed to format the code. Please fix the issues and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#git add .
|
||||
|
||||
# 允许提交
|
||||
exit 0
|
||||
pnpm pretty-quick --staged
|
||||
|
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 运行 clippy
|
||||
# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
|
||||
|
||||
# 如果 clippy 失败,阻止 push
|
||||
# if [ $? -ne 0 ]; then
|
||||
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# 允许 push
|
||||
exit 0
|
@ -38,7 +38,7 @@ npm install pnpm -g
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Download the Mihomo Core Binary
|
||||
### Download the Clash Mihomo Core Binary
|
||||
|
||||
You have two options for downloading the clash binary:
|
||||
|
||||
@ -48,7 +48,7 @@ You have two options for downloading the clash binary:
|
||||
# Use '--force' to force update to the latest version
|
||||
# pnpm run check --force
|
||||
```
|
||||
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
- Manually download it from the [Clash Meta release](https://github.com/MetaCubeX/Clash.Meta/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
|
||||
### Run the Development Server
|
||||
|
||||
@ -65,35 +65,11 @@ pnpm dev:diff
|
||||
To build this project:
|
||||
|
||||
```shell
|
||||
pnpm build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
For a faster build, use the following command
|
||||
|
||||
```shell
|
||||
pnpm build:fast
|
||||
```
|
||||
|
||||
This uses Rust's fast-release profile which significantly reduces compilation time by disabling optimization and LTO. The resulting binary will be larger and less performant than the standard build, but it's useful for testing changes quickly.
|
||||
|
||||
The `Artifacts` will display in the `log` in the Terminal.
|
||||
|
||||
### Build clean
|
||||
|
||||
To clean rust build:
|
||||
|
||||
```shell
|
||||
pnpm clean
|
||||
```
|
||||
|
||||
### Portable Version (Windows Only)
|
||||
|
||||
To package portable version after the build:
|
||||
|
||||
```shell
|
||||
pnpm portable
|
||||
```
|
||||
|
||||
## Contributing Your Changes
|
||||
|
||||
Once you have made your changes:
|
||||
|
@ -55,10 +55,6 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### Donation
|
||||
|
||||
[捐助Clash Verge Rev的开发](https://github.com/sponsors/clash-verge-rev)
|
||||
|
||||
## Development
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||
|
371
UPDATELOG.md
371
UPDATELOG.md
@ -1,373 +1,16 @@
|
||||
## v2.2.3
|
||||
|
||||
#### 已知问题
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
|
||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
|
||||
|
||||
### 2.2.3 相对于 2.2.2
|
||||
#### 修复了:
|
||||
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
|
||||
- “开机自启”和“DNS覆写”开关跳动问题
|
||||
- 自定义托盘图标未能应用更改
|
||||
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
|
||||
- MacOS 托盘速率显示不全
|
||||
- Linux 在系统服务模式下无法拉起 Mihomo 内核
|
||||
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
|
||||
- 相同节点名称可能导致的页面渲染出错
|
||||
- URL Schemes被截断的问题
|
||||
- 首页流量统计卡更好的时间戳范围
|
||||
- 静默启动无法触发自动轻量化计时器
|
||||
|
||||
#### 新增了:
|
||||
- Mihomo(Meta)内核升级至 1.19.4
|
||||
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
|
||||
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
|
||||
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
|
||||
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
|
||||
- 代理组显示节点数量
|
||||
- 统一运行模式检测,支持管理员模式下开启TUN模式
|
||||
- 托盘切换代理模式会根据设置自动断开之前连接
|
||||
- 如订阅获取失败回退使用Clash内核代理再次尝试
|
||||
|
||||
#### 移除了:
|
||||
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
|
||||
|
||||
#### 优化了:
|
||||
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
||||
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
|
||||
- 优化首页流量图表代码,调整图表文字边距
|
||||
- MacOS 托盘速率更好的显示样式和更新逻辑
|
||||
- 首页仅在有流量图表时显示流量图表区域
|
||||
- 更新DNS默认覆写配置
|
||||
- 移除测试目录,简化资源初始化逻辑
|
||||
|
||||
## v2.2.2
|
||||
|
||||
**发行代号:拓**
|
||||
|
||||
感谢 Tunglies 对 Verge 后端重构,性能优化做出的重大贡献!
|
||||
|
||||
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
||||
|
||||
#### 已知问题
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
|
||||
### 2.2.2 相对于 2.2.1(已下架不再提供)
|
||||
#### 修复了:
|
||||
- 弹黑框的问题(原因是服务崩溃触发重装机制)
|
||||
- MacOS进入轻量模式以后隐藏Dock图标
|
||||
- 增加轻量模式缺失的tray翻译
|
||||
- Linux下的窗口边框被削掉的问题
|
||||
|
||||
#### 新增了:
|
||||
- 加强服务检测和重装逻辑
|
||||
- 增强内核与服务保活机制
|
||||
- 增加服务模式下的僵尸进程清理机制
|
||||
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
||||
|
||||
### 2.2.1 相对于 2.2.0(已下架不再提供)
|
||||
#### 修复了:
|
||||
1. **首页**
|
||||
- 修复 Direct 模式首页无法渲染
|
||||
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
||||
- 修复 系统代理标识判断不准的问题
|
||||
- 修复 系统代理地址错误的问题
|
||||
- 代理模式“多余的切换动画”
|
||||
2. **系统**
|
||||
- 修复 MacOS 无法使用快捷键粘贴/选择/复制订阅地址。
|
||||
- 修复 代理端口设置同步问题。
|
||||
- 修复 Linux 无法与 Mihomo 核心 和 ClashVergeRev 服务通信
|
||||
3. **界面**
|
||||
- 修复 连接详情卡没有跟随主题色
|
||||
4. **轻量模式**
|
||||
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
|
||||
|
||||
#### 新增了:
|
||||
1. **首页**
|
||||
- 首页文本过长自动截断
|
||||
2. **轻量模式**
|
||||
- 新增托盘进入轻量模式支持
|
||||
- 新增进入轻量模式快捷键支持
|
||||
3. **系统**
|
||||
- 在 ClashVergeRev 对 Mihomo 进行操作时,总是尝试确保两者运行
|
||||
- 服务器模式下启动mihomo内核的时候查找并停止其他已经存在的内核进程,防止内核假死等问题带来的通信失败
|
||||
4. **托盘**
|
||||
- 新增 MacOS 启用托盘速率显示时,可选隐藏托盘图标显示
|
||||
|
||||
---
|
||||
|
||||
## 2.2.0(已下架不再提供)
|
||||
|
||||
#### 新增功能
|
||||
1. **首页**
|
||||
- 新增首页功能,默认启动页面改为首页。
|
||||
- 首页流量图卡片显示上传/下载名称。
|
||||
- 首页支持轻量模式切换。
|
||||
- 流量统计数据持久保存。
|
||||
- 限制首页配置文件卡片URL长度。
|
||||
|
||||
2. **DNS 设置与覆写**
|
||||
- 新增 DNS 覆写功能。
|
||||
- 默认启用 DNS 覆写。
|
||||
|
||||
3. **解锁测试**
|
||||
- 新增解锁测试页面。
|
||||
|
||||
4. **轻量模式**
|
||||
- 新增轻量模式及设置。
|
||||
- 添加自动轻量模式定时器。
|
||||
|
||||
5. **系统支持**
|
||||
- Mihomo(meta)内核升级 1.19.3
|
||||
- macOS 支持 CMD+W 关闭窗口。
|
||||
- 新增 macOS 应用菜单。
|
||||
- 添加 macOS 安装服务时候的管理员权限提示。
|
||||
- 新增 sidecar(用户空间启动内核) 模式。
|
||||
|
||||
6. **其他**
|
||||
- 增强延迟测试日志和错误处理。
|
||||
- 添加诊断信息导出。
|
||||
- 新增代理命令。
|
||||
|
||||
#### 修复
|
||||
1. **系统**
|
||||
- 修复 Windows 热键崩溃。
|
||||
- 修复 macOS 无框标题。
|
||||
- 修复 macOS 静默启动崩溃。
|
||||
- 修复 macOS tray图标错位到左上角的问题。
|
||||
- 修复 Windows/Linux 运行时崩溃。
|
||||
- 修复 Win10 阴影和边框问题。
|
||||
- 修复 升级或重装后开机自启状态检测和同步问题。
|
||||
|
||||
2. **构建**
|
||||
- 修复构建失败问题。
|
||||
|
||||
#### 优化
|
||||
1. **性能**
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化首页组件性能。
|
||||
- 优化流量图表资源使用。
|
||||
- 提升代理组列表滚动性能。
|
||||
- 加快应用退出速度。
|
||||
- 加快进入轻量模式速度。
|
||||
- 优化小数值速度更新。
|
||||
- 增加请求超时至 60 秒。
|
||||
- 修复代理节点选择同步。
|
||||
- 优化修改verge配置性能。
|
||||
|
||||
2. **重构**
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化定时器管理。
|
||||
- 重构 MihomoManager 处理流量。
|
||||
- 优化 WebSocket 连接。
|
||||
|
||||
3. **其他**
|
||||
- 更新依赖。
|
||||
- 默认 TUN 堆栈改为 gvisor。
|
||||
|
||||
---
|
||||
|
||||
## v2.1.2
|
||||
|
||||
**发行代号:臻**
|
||||
|
||||
代号释义: 千锤百炼臻至善,集性能跃升、功能拓展、交互焕新于一体,彰显持续打磨、全方位优化的迭代精神。
|
||||
|
||||
感谢 Tychristine 对社区群组管理做出的重大贡献!
|
||||
|
||||
##### 2.1.2相对2.1.1(已下架不再提供)更新了:
|
||||
|
||||
- 无法更新和签名验证失败的问题(该死的CDN缓存)
|
||||
- 设置菜单区分Verge基本设置和高级设置
|
||||
- 增加v2 Updater的更多功能和权限
|
||||
- 退出Verge后Tun代理状态仍保留的问题
|
||||
|
||||
##### 2.1.1相对2.1.0(已下架不再提供)更新了:
|
||||
|
||||
- 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限)
|
||||
- MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies)
|
||||
- 文件类型判断不准导致脚本检测报错的问题
|
||||
- 打开Win下的阴影(Win10因底层兼容性问题,可能圆角和边框显示不太完美)
|
||||
- 边框去白边
|
||||
- 修复Linux下编译问题
|
||||
- 修复热键无法关闭面板的问题
|
||||
|
||||
##### 2.1.0 - 发行代号:臻
|
||||
|
||||
### 功能新增
|
||||
|
||||
- 新增窗口状态实时监控与自动保存功能
|
||||
- 增强核心配置变更时的验证与错误处理机制
|
||||
- 支持通过环境变量`CLASH_VERGE_REV_IP`自定义复制IP地址
|
||||
- 添加连接表列宽持久化设置与进程过滤功能
|
||||
- 新增代理组首字母导航与动态滚动定位功能
|
||||
- 实现连接追踪暂停/恢复功能
|
||||
- 支持从托盘菜单快速切换代理配置
|
||||
- 添加轻量级模式开关选项
|
||||
- 允许用户自定义TUN模式增强类型和FakeIP范围
|
||||
- 新增系统代理状态指示器
|
||||
- 增加Alpha版本自动重命名逻辑
|
||||
- 优化字母导航工具提示与防抖交互机制
|
||||
|
||||
### 性能优化
|
||||
|
||||
- 重构代理列表渲染逻辑,提升布局计算效率
|
||||
- 优化代理数据更新机制,采用乐观UI策略
|
||||
- 改进虚拟列表渲染性能(Virtuoso)
|
||||
- 提升主窗口Clash模式切换速度(感谢Tunglies)
|
||||
- 加速内核关闭流程并优化管理逻辑
|
||||
- 优化节点延迟刷新速率
|
||||
- 改进托盘网速显示更新逻辑
|
||||
- 提升配置验证错误信息的可读性
|
||||
- 重构服务架构,优化代码组织结构(感谢Tunglies)
|
||||
- 优化内核启动时的配置验证流程
|
||||
|
||||
### 问题修复
|
||||
|
||||
- 修复删除节点时关联组信息残留问题
|
||||
- 解决菜单切换异常与重复勾选问题
|
||||
- 修正连接页流量计算错误
|
||||
- 修复Windows圆角显示异常问题
|
||||
- 解决控制台废弃API警告
|
||||
- 修复全局热键空值导致的崩溃
|
||||
- 修复Alpha版本Windows打包重命名问题
|
||||
- 修复MacOS端口切换崩溃问题
|
||||
- 解决Linux持续集成更新器问题
|
||||
- 修复静默启动后热键失效问题
|
||||
- 修正TypeScript代理组类型定义
|
||||
- 修复Windows托盘图标空白问题
|
||||
- 优化远程目标地址显示(替换旧版IP展示)
|
||||
|
||||
### 交互体验
|
||||
|
||||
- 统一多平台托盘图标点击行为
|
||||
- 优化代理列表滚动流畅度
|
||||
- 改进日志搜索功能与数据管理
|
||||
- 重构热键管理逻辑,修复托盘冻结问题
|
||||
- 优化托盘网速显示样式
|
||||
- 增强字母导航工具提示的动态响应
|
||||
|
||||
### 国际化
|
||||
|
||||
- 新增配置检查多语言支持
|
||||
- 添加轻量级模式多语言文本
|
||||
- 完善多语言翻译内容
|
||||
|
||||
### 维护更新
|
||||
|
||||
- 将默认TUN协议栈改为gVisor
|
||||
- 更新Node.js运行版本
|
||||
- 移除自动生成更新器文件
|
||||
- 清理废弃代码与未使用组件
|
||||
- 禁用工作流自动Alpha标签更新
|
||||
- 更新依赖库版本
|
||||
- 添加MacOS格式转换函数专项测试
|
||||
- 优化开发模式日志输出
|
||||
|
||||
### 安全增强
|
||||
|
||||
- 强化应用启动时的配置验证机制
|
||||
- 改进脚本验证与异常处理流程
|
||||
- 修复编译警告(移除无用导入)
|
||||
|
||||
---
|
||||
|
||||
## v2.0.3
|
||||
## v2.0.0
|
||||
|
||||
### Notice
|
||||
|
||||
- !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!!
|
||||
- 历时3个月的紧密开发与严格测试稳定版2.0.0终于发布了:巨量改进与性能、稳定性提升,目前Clash Verge Rev已经有了比肩cfw的健壮性;而且更强大易用!
|
||||
- 由于更改了服务安装逻辑,每次更新安装需要输入系统密码卸载老版本服务和安装新版本服务,以后可以丝滑使用tun(虚拟网卡)模式
|
||||
|
||||
### 2.0.3相对于2.0.2改进修复了:
|
||||
|
||||
1. 修复VLess-URL识别网络类型错误 f400f90 #2126
|
||||
2. 新增系统代理绕过文本校验 c71e18e
|
||||
3. 修复脚本编辑器UI显示不正确 6197249 #2267
|
||||
4. 修复Shift热键无效 589324b #2278
|
||||
5. 新增nushell环境变量复制 d233a84
|
||||
6. 修复全局扩展脚本无法覆写DNS d22b37c #2235
|
||||
7. 切换到系统代理相对于稳定的版本 38745d4
|
||||
8. 修改fake-ip-range网段 0e3b631
|
||||
9. 修复窗口隐藏后WebSocket未断开连接,减小内存风险 b42d13f
|
||||
10. 改进系统代理绕过设置 c5c840d
|
||||
11. 修复i18n翻译文本缺失 b149084
|
||||
12. 修复双击托盘图标打开面板 f839d3b #2346
|
||||
13. 修复Windows10窗口白色边框 4f6ca40 #2425
|
||||
14. 修复Windows窗口状态恢复 4f6ca40
|
||||
15. 改进保存配置文件自动重启Mihomo内核 0669f7a
|
||||
16. 改进更新托盘图标性能 d9291d4
|
||||
17. 修复保存配置后代理列表未更新 542baf9 #2460
|
||||
18. 新增MacOS托盘显示实时速率,可在"界面设置"中关闭 1b2f1b6
|
||||
19. 新增托盘菜单显示已设置的快捷键 eeff4d4
|
||||
20. 新增重载配置文件错误响应"400"时显示更多错误信息 c5989d2 #2492
|
||||
21. 修复GUI代理状态与菜单显示不一致 13b63b5 #2502
|
||||
22. 新增默认语言跟随系统语言(无语言支持即为英语),添加了阿拉伯语、印尼语、鞑靼语支持 9655f77 #2940
|
||||
|
||||
### Features
|
||||
|
||||
- Meta(mihomo)内核升级 1.19.1
|
||||
- 增加更多语言和托盘语言跟随
|
||||
- MacOS增加状态栏速率显示
|
||||
- 托盘显示快捷键
|
||||
- 重载配置文件错误响应"400"时显示更多错误信息
|
||||
- 改进保存配置文件自动重启Mihomo内核
|
||||
|
||||
### Performance
|
||||
|
||||
- 改进更新托盘图标性能
|
||||
- 窗口隐藏后WebSocket断开连接
|
||||
|
||||
---
|
||||
|
||||
## v2.0.2
|
||||
|
||||
### Notice
|
||||
|
||||
- !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!!
|
||||
- 历时3个月的紧密开发与严格测试稳定版2.0.0终于发布了:巨量改进与性能、稳定性提升,目前Clash Verge Rev已经有了比肩cfw的健壮性;而且更强大易用!
|
||||
- 由于更改了服务安装逻辑,Mac/Linux 首次安装需要输入系统密码卸载和安装服务,以后可以丝滑使用 tun(虚拟网卡)模式
|
||||
- 因 Tauri 2.0 底层 bug,关闭窗口后保留webview进程,优点是再次打开面板更快,缺点是内存使用略有增加
|
||||
|
||||
### 2.0.2相对于2.0.1改进了:
|
||||
|
||||
- MacOS 下自定义图标可以支持彩色、单色切换
|
||||
- 修正了 Linux 下多个内核僵尸进程的问题
|
||||
- 修正了 DNS ipv6 强制覆盖的逻辑
|
||||
- 修改了 MacOS tun 模式下覆盖设置 dns 字段的问题
|
||||
- 修正了 MacOS tray 图标不会随代理模式更改的问题
|
||||
- 静默启动下重复运行会出现多个实例的bug
|
||||
- 安装的时候自动删除历史残留启动项
|
||||
- Tun模式默认是还用内核推荐的 mixed 堆栈
|
||||
- 改进了默认窗口大小(启动软件窗口不会那么小了)
|
||||
- 改进了 WebDAV 备份超时时间机制
|
||||
- 测试菜单添加滚动条
|
||||
- 改进和修正了 Tun 模式下对设置的覆盖逻辑
|
||||
- 修复了打开配置出错的问题
|
||||
- 修复了配置文件无法拖拽添加的问题
|
||||
- 改善了浅色模式的对比度
|
||||
|
||||
### 2.0.1相对于2.0.0改进了:
|
||||
|
||||
- 无法从 2.0rc和2.0.0 升级的问题(已经安装了2.0版本的需手动下载安装)
|
||||
- MacOS 系统下少有的无法安装服务,无法启动的问题,目前更健壮了
|
||||
- 当系统中没有 yaml 编辑器的情况下,打开文件程序崩溃的问题
|
||||
- Windows 应用内升级和覆盖安装不会删除老执行文件的问题
|
||||
- 修改优化了 mac 下 fakeip 段和 dns
|
||||
- 测试菜单 svg 图标格式检查
|
||||
- 应用内升级重复安装 vs runtime 的问题
|
||||
- 修复外部控制下密码有特殊字符认证出错的问题
|
||||
- 修复恢复 Webdav 备份设置后, Webdav 设置丢失的问题
|
||||
- 代理页面增加快速回到顶部的按钮
|
||||
- 由于更改了服务安装逻辑,Mac/Linux 首次安装需要输入 2 遍系统密码卸载和安装服务,以后可以丝滑使用 tun(虚拟网卡)模式
|
||||
- 因 Tauri 2.0 底层 bug,关闭窗口暂时修改为最小化功能
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- 重大框架升级:使用 Tauri 2.0(巨量改进与性能提升)
|
||||
- 强烈建议完全删除 1.x 老版本再安装此版本
|
||||
- 出现 bug 到 issues 中提出;以后不再接受1.x版本的bug反馈。
|
||||
- 强烈建议完全删除 1.x 老版本再安装此版本 !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!!
|
||||
|
||||
### Features
|
||||
|
||||
@ -387,7 +30,7 @@
|
||||
- 添加统一延迟的设置开关
|
||||
- 添加 Windows 下自动检测并下载 vc runtime 的功能
|
||||
- 支持显示 mux 和 mptcp 的节点标识
|
||||
- 延迟测试连接更换 http 的 cp.cloudflare.com/generate_204 (关闭统一延迟的情况下延迟测试结果会有所增加)
|
||||
- 延迟测试连接更换 https 的 cp.cloudflare.com/generate_204 以防止机场劫持(关闭统一延迟的情况下延迟测试结果会有所增加)。
|
||||
- 重构日志记录逻辑,可以收集和筛选所有日志类型了(之前无法记录debug的日志类型)
|
||||
|
||||
### Performance
|
||||
@ -415,10 +58,10 @@
|
||||
- 修复快捷键设置的相关 bug
|
||||
- 修复 Win 下点左键菜单闪现的问题(Mac 下的操作逻辑相反,默认情况下不管点左/右键均会打开菜单,闪现不属于 bug)
|
||||
|
||||
### Known issues
|
||||
### Know issues
|
||||
|
||||
- Windows 下窗口大小无法记忆(等待上游修复)
|
||||
- Webdav 备份因为安全性和兼容性问题,暂不支持跨平台配置同步
|
||||
- Webdav 备份因为安全性和兼容性问题,暂不支持同步 Webdav 服务器地址和登录信息;跨平台配置同步
|
||||
|
||||
---
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 166 KiB |
Binary file not shown.
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 162 KiB |
110
package.json
110
package.json
@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.2.3",
|
||||
"version": "2.0.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
||||
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
|
||||
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
|
||||
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsc --noEmit && vite build",
|
||||
@ -16,89 +15,82 @@
|
||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||
"portable": "node scripts/portable.mjs",
|
||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||
"release-version": "node scripts/release_version.mjs",
|
||||
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
||||
"prepare": "husky",
|
||||
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml"
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^6.4.8",
|
||||
"@mui/lab": "6.0.0-beta.25",
|
||||
"@mui/material": "^6.4.8",
|
||||
"@mui/x-data-grid": "^7.28.0",
|
||||
"@tauri-apps/api": "2.2.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||
"@tauri-apps/plugin-notification": "^2.2.2",
|
||||
"@tauri-apps/plugin-process": "^2.2.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.0",
|
||||
"@tauri-apps/plugin-updater": "2.3.0",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@mui/icons-material": "^6.1.6",
|
||||
"@mui/lab": "5.0.0-alpha.149",
|
||||
"@mui/material": "^6.1.6",
|
||||
"@mui/x-data-grid": "^7.22.2",
|
||||
"@tauri-apps/api": "2.1.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-fs": "^2.0.2",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.0.0",
|
||||
"@tauri-apps/plugin-notification": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.8.3",
|
||||
"ahooks": "^3.8.1",
|
||||
"axios": "^1.7.7",
|
||||
"cli-color": "^2.0.4",
|
||||
"d3-shape": "^3.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.44",
|
||||
"glob": "^11.0.1",
|
||||
"i18next": "^24.2.3",
|
||||
"foxact": "^0.2.41",
|
||||
"glob": "^11.0.0",
|
||||
"i18next": "^23.16.5",
|
||||
"js-base64": "^3.7.7",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "^5.3.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"peggy": "^4.2.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"monaco-yaml": "^5.2.3",
|
||||
"nanoid": "^5.0.8",
|
||||
"peggy": "^4.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-markdown": "^9.1.0",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-i18next": "^15.1.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-monaco-editor": "^0.56.2",
|
||||
"react-router-dom": "^6.30.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^4.12.5",
|
||||
"recharts": "^2.15.1",
|
||||
"react-virtuoso": "^4.12.0",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.3.3",
|
||||
"swr": "^2.2.5",
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.3"
|
||||
"zustand": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.0",
|
||||
"@tauri-apps/cli": "2.2.7",
|
||||
"@tauri-apps/cli": "2.1.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@vitejs/plugin-legacy": "^6.0.2",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-transition-group": "^4.4.11",
|
||||
"@vitejs/plugin-legacy": "^5.4.3",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"husky": "^9.1.7",
|
||||
"meta-json-schema": "^1.19.3",
|
||||
"meta-json-schema": "^1.18.10",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-quick": "^4.1.1",
|
||||
"sass": "^1.86.0",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.2",
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-quick": "^4.0.0",
|
||||
"sass": "^1.81.0",
|
||||
"terser": "^5.36.0",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
|
8413
pnpm-lock.yaml
generated
8413
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@ import { extract } from "tar";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import fetch from "node-fetch";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import proxyAgent from "https-proxy-agent";
|
||||
import { execSync } from "child_process";
|
||||
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
|
||||
import { glob } from "glob";
|
||||
@ -85,7 +85,7 @@ async function getLatestAlphaVersion() {
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
options.agent = proxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_ALPHA_VERSION_URL, {
|
||||
@ -132,7 +132,7 @@ async function getLatestReleaseVersion() {
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
options.agent = proxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_VERSION_URL, {
|
||||
@ -153,13 +153,13 @@ async function getLatestReleaseVersion() {
|
||||
*/
|
||||
if (!META_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`,
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`,
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`
|
||||
);
|
||||
}
|
||||
|
||||
@ -332,7 +332,7 @@ async function resolveResource(binInfo) {
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
options.agent = proxyAgent(httpProxy);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
@ -354,7 +354,7 @@ const resolvePlugin = async () => {
|
||||
const tempDir = path.join(TEMP_DIR, "SimpleSC");
|
||||
const tempZip = path.join(
|
||||
tempDir,
|
||||
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip",
|
||||
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip"
|
||||
);
|
||||
const tempDll = path.join(tempDir, "SimpleSC.dll");
|
||||
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
|
||||
@ -398,34 +398,6 @@ const resolveServicePermission = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 在 resolveResource 函数后添加新函数
|
||||
async function resolveLocales() {
|
||||
const srcLocalesDir = path.join(cwd, "src/locales");
|
||||
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
|
||||
|
||||
try {
|
||||
// 确保目标目录存在
|
||||
await fsp.mkdir(targetLocalesDir, { recursive: true });
|
||||
|
||||
// 读取所有语言文件
|
||||
const files = await fsp.readdir(srcLocalesDir);
|
||||
|
||||
// 复制每个文件
|
||||
for (const file of files) {
|
||||
const srcPath = path.join(srcLocalesDir, file);
|
||||
const targetPath = path.join(targetLocalesDir, file);
|
||||
|
||||
await fsp.copyFile(srcPath, targetPath);
|
||||
log_success(`Copied locale file: ${file}`);
|
||||
}
|
||||
|
||||
log_success("All locale files copied successfully");
|
||||
} catch (err) {
|
||||
log_error("Error copying locale files:", err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* main
|
||||
*/
|
||||
@ -516,8 +488,8 @@ const tasks = [
|
||||
{
|
||||
name: "service_chmod",
|
||||
func: resolveServicePermission,
|
||||
retry: 5,
|
||||
unixOnly: platform === "linux" || platform === "darwin",
|
||||
retry: 1,
|
||||
unixOnly: true,
|
||||
},
|
||||
{
|
||||
name: "windows-sysproxy",
|
||||
@ -537,20 +509,15 @@ const tasks = [
|
||||
retry: 5,
|
||||
macosOnly: true,
|
||||
},
|
||||
{
|
||||
name: "locales",
|
||||
func: resolveLocales,
|
||||
retry: 2,
|
||||
},
|
||||
];
|
||||
|
||||
async function runTask() {
|
||||
const task = tasks.shift();
|
||||
if (!task) return;
|
||||
if (task.unixOnly && platform === "win32") return runTask();
|
||||
if (task.winOnly && platform !== "win32") return runTask();
|
||||
if (task.macosOnly && platform !== "darwin") return runTask();
|
||||
if (task.linuxOnly && platform !== "linux") return runTask();
|
||||
if (task.unixOnly && platform === "win32") return runTask();
|
||||
if (task.macosOnly && platform !== "darwin") return runTask();
|
||||
|
||||
for (let i = 0; i < task.retry; i++) {
|
||||
try {
|
||||
@ -565,3 +532,4 @@ async function runTask() {
|
||||
}
|
||||
|
||||
runTask();
|
||||
runTask();
|
||||
|
@ -1,56 +0,0 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 为Alpha版本重命名版本号
|
||||
*/
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
/**
|
||||
* 标准输出HEAD hash
|
||||
*/
|
||||
async function getLatestCommitHash() {
|
||||
try {
|
||||
const { stdout } = await execPromise("git rev-parse HEAD");
|
||||
const commitHash = stdout.trim();
|
||||
// 格式化,只截取前7位字符
|
||||
const formathash = commitHash.substring(0, 7);
|
||||
console.log(`Found the latest commit hash code: ${commitHash}`);
|
||||
return formathash;
|
||||
} catch (error) {
|
||||
console.error("pnpm run fix-alpha-version ERROR", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string 传入格式化后的hash
|
||||
* 将新的版本号写入文件 package.json
|
||||
*/
|
||||
async function updatePackageVersion(newVersion) {
|
||||
// 获取内容根目录
|
||||
const _dirname = process.cwd();
|
||||
const packageJsonPath = path.join(_dirname, "package.json");
|
||||
try {
|
||||
// 读取文件
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
// 获取键值替换
|
||||
let result = packageJson.version.replace("alpha", newVersion);
|
||||
console.log("[INFO]: Current version is: ", result);
|
||||
packageJson.version = result;
|
||||
// 写入版本号
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: Alpha version update to: ${newVersion}`);
|
||||
} catch (error) {
|
||||
console.error("pnpm run fix-alpha-version ERROR", error);
|
||||
}
|
||||
}
|
||||
|
||||
const newVersion = await getLatestCommitHash();
|
||||
updatePackageVersion(newVersion).catch(console.error);
|
@ -49,9 +49,9 @@ async function resolvePortable() {
|
||||
zip.addLocalFolder(
|
||||
path.join(
|
||||
releaseDir,
|
||||
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
|
||||
),
|
||||
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
|
||||
);
|
||||
zip.addLocalFolder(configDir, ".config");
|
||||
|
||||
|
@ -2,16 +2,20 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { createRequire } from "module";
|
||||
import fsp from "fs/promises";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
|
||||
const target = process.argv.slice(2)[0];
|
||||
const alpha = process.argv.slice(2)[1];
|
||||
|
||||
const ARCH_MAP = {
|
||||
"x86_64-pc-windows-msvc": "x64",
|
||||
"i686-pc-windows-msvc": "x86",
|
||||
"aarch64-pc-windows-msvc": "arm64",
|
||||
};
|
||||
|
||||
const PROCESS_MAP = {
|
||||
x64: "x64",
|
||||
ia32: "x86",
|
||||
arm64: "arm64",
|
||||
};
|
||||
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch];
|
||||
@ -33,9 +37,10 @@ async function resolvePortable() {
|
||||
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
|
||||
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
|
||||
}
|
||||
|
||||
const zip = new AdmZip();
|
||||
|
||||
zip.addLocalFile(path.join(releaseDir, "clash-verge.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
|
||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||
@ -44,9 +49,46 @@ async function resolvePortable() {
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJson = require("../package.json");
|
||||
const { version } = packageJson;
|
||||
|
||||
const zipFile = `Clash.Verge_${version}_${arch}_portable.zip`;
|
||||
zip.writeZip(zipFile);
|
||||
|
||||
console.log("[INFO]: create portable zip successfully");
|
||||
|
||||
// push release assets
|
||||
if (process.env.GITHUB_TOKEN === undefined) {
|
||||
throw new Error("GITHUB_TOKEN is required");
|
||||
}
|
||||
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
|
||||
console.log("[INFO]: upload to ", tag);
|
||||
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag,
|
||||
});
|
||||
|
||||
let assets = release.assets.filter((x) => {
|
||||
return x.name === zipFile;
|
||||
});
|
||||
if (assets.length > 0) {
|
||||
let id = assets[0].id;
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
asset_id: id,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(release.name);
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: release.id,
|
||||
name: zipFile,
|
||||
data: zip.toBuffer(),
|
||||
});
|
||||
}
|
||||
|
||||
resolvePortable().catch(console.error);
|
||||
|
@ -1,96 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 更新 package.json 文件中的版本号
|
||||
*/
|
||||
async function updatePackageVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const packageJsonPath = path.join(_dirname, "package.json");
|
||||
try {
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
|
||||
let result = packageJson.version;
|
||||
if (!result.includes("alpha")) {
|
||||
result = `${result}-alpha`;
|
||||
}
|
||||
|
||||
console.log("[INFO]: Current package.json version is: ", result);
|
||||
packageJson.version = result;
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: package.json version updated to: ${result}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating package.json version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Cargo.toml 文件中的版本号
|
||||
*/
|
||||
async function updateCargoVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
|
||||
try {
|
||||
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||
const lines = data.split("\n");
|
||||
|
||||
const updatedLines = lines.map((line) => {
|
||||
if (line.startsWith("version =")) {
|
||||
const versionMatch = line.match(/version\s*=\s*"([^"]+)"/);
|
||||
if (versionMatch && !versionMatch[1].includes("alpha")) {
|
||||
const newVersion = `${versionMatch[1]}-alpha`;
|
||||
return line.replace(versionMatch[1], newVersion);
|
||||
}
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||
} catch (error) {
|
||||
console.error("Error updating Cargo.toml version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 tauri.conf.json 文件中的版本号
|
||||
*/
|
||||
async function updateTauriConfigVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
|
||||
try {
|
||||
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||
const tauriConfig = JSON.parse(data);
|
||||
|
||||
let version = tauriConfig.version;
|
||||
if (!version.includes("alpha")) {
|
||||
version = `${version}-alpha`;
|
||||
}
|
||||
|
||||
console.log("[INFO]: Current tauri.conf.json version is: ", version);
|
||||
tauriConfig.version = version;
|
||||
await fs.writeFile(
|
||||
tauriConfigPath,
|
||||
JSON.stringify(tauriConfig, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: tauri.conf.json version updated to: ${version}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating tauri.conf.json version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数,依次更新所有文件的版本号
|
||||
*/
|
||||
async function main() {
|
||||
await updatePackageVersion();
|
||||
await updateCargoVersion();
|
||||
await updateTauriConfigVersion();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
@ -1,197 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { program } from "commander";
|
||||
|
||||
/**
|
||||
* 验证版本号格式
|
||||
* @param {string} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidVersion(version) {
|
||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?$/i.test(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化版本号(确保v前缀可选)
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeVersion(version) {
|
||||
return version.startsWith("v") ? version : `v${version}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 package.json 文件中的版本号
|
||||
* @param {string} newVersion 新版本号
|
||||
*/
|
||||
async function updatePackageVersion(newVersion) {
|
||||
const _dirname = process.cwd();
|
||||
const packageJsonPath = path.join(_dirname, "package.json");
|
||||
try {
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
|
||||
console.log(
|
||||
"[INFO]: Current package.json version is: ",
|
||||
packageJson.version,
|
||||
);
|
||||
packageJson.version = newVersion.startsWith("v")
|
||||
? newVersion.slice(1)
|
||||
: newVersion;
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(
|
||||
`[INFO]: package.json version updated to: ${packageJson.version}`,
|
||||
);
|
||||
return packageJson.version;
|
||||
} catch (error) {
|
||||
console.error("Error updating package.json version:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Cargo.toml 文件中的版本号
|
||||
* @param {string} newVersion 新版本号
|
||||
*/
|
||||
async function updateCargoVersion(newVersion) {
|
||||
const _dirname = process.cwd();
|
||||
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
|
||||
try {
|
||||
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||
const lines = data.split("\n");
|
||||
|
||||
const versionWithoutV = newVersion.startsWith("v")
|
||||
? newVersion.slice(1)
|
||||
: newVersion;
|
||||
|
||||
const updatedLines = lines.map((line) => {
|
||||
if (line.trim().startsWith("version =")) {
|
||||
return line.replace(
|
||||
/version\s*=\s*"[^"]+"/,
|
||||
`version = "${versionWithoutV}"`,
|
||||
);
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating Cargo.toml version:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 tauri.conf.json 文件中的版本号
|
||||
* @param {string} newVersion 新版本号
|
||||
*/
|
||||
async function updateTauriConfigVersion(newVersion) {
|
||||
const _dirname = process.cwd();
|
||||
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
|
||||
try {
|
||||
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||
const tauriConfig = JSON.parse(data);
|
||||
|
||||
const versionWithoutV = newVersion.startsWith("v")
|
||||
? newVersion.slice(1)
|
||||
: newVersion;
|
||||
|
||||
console.log(
|
||||
"[INFO]: Current tauri.conf.json version is: ",
|
||||
tauriConfig.version,
|
||||
);
|
||||
tauriConfig.version = versionWithoutV;
|
||||
await fs.writeFile(
|
||||
tauriConfigPath,
|
||||
JSON.stringify(tauriConfig, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(
|
||||
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error updating tauri.conf.json version:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前版本号(从package.json)
|
||||
*/
|
||||
async function getCurrentVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const packageJsonPath = path.join(_dirname, "package.json");
|
||||
try {
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
return packageJson.version;
|
||||
} catch (error) {
|
||||
console.error("Error getting current version:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数,更新所有文件的版本号
|
||||
* @param {string} versionArg 版本参数(可以是标签或完整版本号)
|
||||
*/
|
||||
async function main(versionArg) {
|
||||
if (!versionArg) {
|
||||
console.error("Error: Version argument is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
let newVersion;
|
||||
const validTags = ["alpha", "beta", "rc"];
|
||||
|
||||
// 判断参数是标签还是完整版本号
|
||||
if (validTags.includes(versionArg.toLowerCase())) {
|
||||
// 标签模式:在当前版本基础上添加标签
|
||||
const currentVersion = await getCurrentVersion();
|
||||
const baseVersion = currentVersion.replace(
|
||||
/-(alpha|beta|rc)(\.\d+)?$/i,
|
||||
"",
|
||||
);
|
||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||
} else {
|
||||
// 完整版本号模式
|
||||
if (!isValidVersion(versionArg)) {
|
||||
console.error(
|
||||
"Error: Invalid version format. Expected format: vX.X.X or vX.X.X-tag (e.g. v2.2.3 or v2.2.3-alpha)",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
newVersion = normalizeVersion(versionArg);
|
||||
}
|
||||
|
||||
console.log(`[INFO]: Updating versions to: ${newVersion}`);
|
||||
await updatePackageVersion(newVersion);
|
||||
await updateCargoVersion(newVersion);
|
||||
await updateTauriConfigVersion(newVersion);
|
||||
console.log("[SUCCESS]: All version updates completed successfully!");
|
||||
} catch (error) {
|
||||
console.error("[ERROR]: Failed to update versions:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Example:
|
||||
// pnpm release-version 2.2.3-alpha
|
||||
// 设置命令行界面
|
||||
program
|
||||
.name("pnpm release-version")
|
||||
.description(
|
||||
"Update project version numbers. Can add tag (alpha/beta/rc) or set full version (e.g. v2.2.3 or v2.2.3-alpha)",
|
||||
)
|
||||
.argument(
|
||||
"<version>",
|
||||
"version tag (alpha/beta/rc) or full version (e.g. v2.2.3 or v2.2.3-alpha)",
|
||||
)
|
||||
.action(main)
|
||||
.parse(process.argv);
|
@ -43,42 +43,3 @@ export async function resolveUpdateLog(tag) {
|
||||
|
||||
return map[tag].join("\n").trim();
|
||||
}
|
||||
|
||||
export async function resolveUpdateLogDefault() {
|
||||
const cwd = process.cwd();
|
||||
const file = path.join(cwd, UPDATE_LOG);
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error("could not found UPDATELOG.md");
|
||||
}
|
||||
|
||||
const data = await fsp.readFile(file, "utf-8");
|
||||
|
||||
const reTitle = /^## v[\d\.]+/;
|
||||
const reEnd = /^---/;
|
||||
|
||||
let isCapturing = false;
|
||||
let content = [];
|
||||
let firstTag = "";
|
||||
|
||||
for (const line of data.split("\n")) {
|
||||
if (reTitle.test(line) && !isCapturing) {
|
||||
isCapturing = true;
|
||||
firstTag = line.slice(3).trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCapturing) {
|
||||
if (reEnd.test(line)) {
|
||||
break;
|
||||
}
|
||||
content.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstTag) {
|
||||
throw new Error("could not found any version tag in UPDATELOG.md");
|
||||
}
|
||||
|
||||
return content.join("\n").trim();
|
||||
}
|
||||
|
@ -1,15 +1,10 @@
|
||||
import fetch from "node-fetch";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||
|
||||
// Add stable update JSON filenames
|
||||
const UPDATE_TAG_NAME = "updater";
|
||||
const UPDATE_JSON_FILE = "update.json";
|
||||
const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
// Add alpha update JSON filenames
|
||||
const ALPHA_TAG_NAME = "updater-alpha";
|
||||
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
||||
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
|
||||
/// generate update.json
|
||||
/// upload to update tag's release asset
|
||||
@ -21,293 +16,182 @@ async function resolveUpdater() {
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
|
||||
// Fetch all tags using pagination
|
||||
let allTags = [];
|
||||
let page = 1;
|
||||
const perPage = 100;
|
||||
const { data: tags } = await github.rest.repos.listTags({
|
||||
...options,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const { data: pageTags } = await github.rest.repos.listTags({
|
||||
...options,
|
||||
per_page: perPage,
|
||||
page: page,
|
||||
});
|
||||
// get the latest publish tag
|
||||
const tag = tags.find((t) => t.name.startsWith("v"));
|
||||
|
||||
allTags = allTags.concat(pageTags);
|
||||
|
||||
// Break if we received fewer tags than requested (last page)
|
||||
if (pageTags.length < perPage) {
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
const tags = allTags;
|
||||
console.log(`Retrieved ${tags.length} tags in total`);
|
||||
|
||||
// More flexible tag detection with regex patterns
|
||||
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
||||
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
|
||||
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
||||
|
||||
// Get the latest stable tag and pre-release tag
|
||||
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
||||
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
||||
|
||||
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
||||
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
||||
console.log(
|
||||
"Pre-release tag:",
|
||||
preReleaseTag ? preReleaseTag.name : "None found",
|
||||
);
|
||||
console.log(tag);
|
||||
console.log();
|
||||
|
||||
// Process stable release
|
||||
if (stableTag) {
|
||||
await processRelease(github, options, stableTag, false);
|
||||
}
|
||||
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: tag.name,
|
||||
});
|
||||
|
||||
// Process pre-release if found
|
||||
if (preReleaseTag) {
|
||||
await processRelease(github, options, preReleaseTag, true);
|
||||
}
|
||||
}
|
||||
const updateData = {
|
||||
name: tag.name,
|
||||
notes: await resolveUpdateLog(tag.name), // use updatelog.md
|
||||
pub_date: new Date().toISOString(),
|
||||
platforms: {
|
||||
win64: { signature: "", url: "" }, // compatible with older formats
|
||||
linux: { signature: "", url: "" }, // compatible with older formats
|
||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||
"darwin-aarch64": { signature: "", url: "" },
|
||||
"darwin-intel": { signature: "", url: "" },
|
||||
"darwin-x86_64": { signature: "", url: "" },
|
||||
"linux-x86_64": { signature: "", url: "" },
|
||||
"linux-x86": { signature: "", url: "" },
|
||||
"linux-i686": { signature: "", url: "" },
|
||||
"linux-aarch64": { signature: "", url: "" },
|
||||
"linux-armv7": { signature: "", url: "" },
|
||||
"windows-x86_64": { signature: "", url: "" },
|
||||
"windows-aarch64": { signature: "", url: "" },
|
||||
"windows-x86": { signature: "", url: "" },
|
||||
"windows-i686": { signature: "", url: "" },
|
||||
},
|
||||
};
|
||||
|
||||
// Process a release (stable or alpha) and generate update files
|
||||
async function processRelease(github, options, tag, isAlpha) {
|
||||
if (!tag) return;
|
||||
const promises = latestRelease.assets.map(async (asset) => {
|
||||
const { name, browser_download_url } = asset;
|
||||
|
||||
try {
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: tag.name,
|
||||
});
|
||||
|
||||
const updateData = {
|
||||
name: tag.name,
|
||||
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||
),
|
||||
pub_date: new Date().toISOString(),
|
||||
platforms: {
|
||||
win64: { signature: "", url: "" }, // compatible with older formats
|
||||
linux: { signature: "", url: "" }, // compatible with older formats
|
||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||
"darwin-aarch64": { signature: "", url: "" },
|
||||
"darwin-intel": { signature: "", url: "" },
|
||||
"darwin-x86_64": { signature: "", url: "" },
|
||||
"linux-x86_64": { signature: "", url: "" },
|
||||
"linux-x86": { signature: "", url: "" },
|
||||
"linux-i686": { signature: "", url: "" },
|
||||
"linux-aarch64": { signature: "", url: "" },
|
||||
"linux-armv7": { signature: "", url: "" },
|
||||
"windows-x86_64": { signature: "", url: "" },
|
||||
"windows-aarch64": { signature: "", url: "" },
|
||||
"windows-x86": { signature: "", url: "" },
|
||||
"windows-i686": { signature: "", url: "" },
|
||||
},
|
||||
};
|
||||
|
||||
const promises = release.assets.map(async (asset) => {
|
||||
const { name, browser_download_url } = asset;
|
||||
|
||||
// Process all the platform URL and signature data
|
||||
// win64 url
|
||||
if (name.endsWith("x64-setup.exe")) {
|
||||
updateData.platforms.win64.url = browser_download_url;
|
||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||
}
|
||||
// win64 signature
|
||||
if (name.endsWith("x64-setup.exe.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.win64.signature = sig;
|
||||
updateData.platforms["windows-x86_64"].signature = sig;
|
||||
}
|
||||
|
||||
// win32 url
|
||||
if (name.endsWith("x86-setup.exe")) {
|
||||
updateData.platforms["windows-x86"].url = browser_download_url;
|
||||
updateData.platforms["windows-i686"].url = browser_download_url;
|
||||
}
|
||||
// win32 signature
|
||||
if (name.endsWith("x86-setup.exe.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["windows-x86"].signature = sig;
|
||||
updateData.platforms["windows-i686"].signature = sig;
|
||||
}
|
||||
|
||||
// win arm url
|
||||
if (name.endsWith("arm64-setup.exe")) {
|
||||
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||
}
|
||||
// win arm signature
|
||||
if (name.endsWith("arm64-setup.exe.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["windows-aarch64"].signature = sig;
|
||||
}
|
||||
|
||||
// darwin url (intel)
|
||||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
||||
updateData.platforms.darwin.url = browser_download_url;
|
||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
||||
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
||||
}
|
||||
// darwin signature (intel)
|
||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.darwin.signature = sig;
|
||||
updateData.platforms["darwin-intel"].signature = sig;
|
||||
updateData.platforms["darwin-x86_64"].signature = sig;
|
||||
}
|
||||
|
||||
// darwin url (aarch)
|
||||
if (name.endsWith("aarch64.app.tar.gz")) {
|
||||
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
||||
// 使linux可以检查更新
|
||||
updateData.platforms.linux.url = browser_download_url;
|
||||
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||
updateData.platforms["linux-aarch64"].url = browser_download_url;
|
||||
updateData.platforms["linux-armv7"].url = browser_download_url;
|
||||
}
|
||||
// darwin signature (aarch)
|
||||
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["darwin-aarch64"].signature = sig;
|
||||
updateData.platforms.linux.signature = sig;
|
||||
updateData.platforms["linux-x86_64"].signature = sig;
|
||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||
updateData.platforms["linux-aarch64"].signature = sig;
|
||||
updateData.platforms["linux-armv7"].signature = sig;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
console.log(updateData);
|
||||
|
||||
// maybe should test the signature as well
|
||||
// delete the null field
|
||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||
if (!value.url) {
|
||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
||||
delete updateData.platforms[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Generate a proxy update file for accelerated GitHub resources
|
||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
||||
|
||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||
if (value.url) {
|
||||
updateDataNew.platforms[key].url =
|
||||
"https://download.clashverge.dev/" + value.url;
|
||||
} else {
|
||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||
}
|
||||
});
|
||||
|
||||
// Get the appropriate updater release based on isAlpha flag
|
||||
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
||||
console.log(
|
||||
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
||||
releaseTag,
|
||||
);
|
||||
|
||||
try {
|
||||
let updateRelease;
|
||||
|
||||
try {
|
||||
// Try to get the existing release
|
||||
const response = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: releaseTag,
|
||||
});
|
||||
updateRelease = response.data;
|
||||
console.log(
|
||||
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||
);
|
||||
} catch (error) {
|
||||
// If release doesn't exist, create it
|
||||
if (error.status === 404) {
|
||||
console.log(
|
||||
`Release with tag ${releaseTag} not found, creating new release...`,
|
||||
);
|
||||
const createResponse = await github.rest.repos.createRelease({
|
||||
...options,
|
||||
tag_name: releaseTag,
|
||||
name: isAlpha
|
||||
? "Auto-update Alpha Channel"
|
||||
: "Auto-update Stable Channel",
|
||||
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
|
||||
prerelease: isAlpha,
|
||||
});
|
||||
updateRelease = createResponse.data;
|
||||
console.log(
|
||||
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||
);
|
||||
} else {
|
||||
// If it's another error, throw it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// File names based on release type
|
||||
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
|
||||
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
||||
|
||||
// Delete existing assets with these names
|
||||
for (let asset of updateRelease.assets) {
|
||||
if (asset.name === jsonFile) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
asset_id: asset.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.name === proxyFile) {
|
||||
await github.rest.repos
|
||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||
.catch(console.error); // do not break the pipeline
|
||||
}
|
||||
}
|
||||
|
||||
// Upload new assets
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: jsonFile,
|
||||
data: JSON.stringify(updateData, null, 2),
|
||||
});
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: proxyFile,
|
||||
data: JSON.stringify(updateDataNew, null, 2),
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
||||
error.message,
|
||||
);
|
||||
// win64 url
|
||||
if (name.endsWith("x64-setup.nsis.zip")) {
|
||||
updateData.platforms.win64.url = browser_download_url;
|
||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
||||
// win64 signature
|
||||
if (name.endsWith("x64-setup.nsis.zip.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.win64.signature = sig;
|
||||
updateData.platforms["windows-x86_64"].signature = sig;
|
||||
}
|
||||
|
||||
// win32 url
|
||||
if (name.endsWith("x86-setup.nsis.zip")) {
|
||||
updateData.platforms["windows-x86"].url = browser_download_url;
|
||||
updateData.platforms["windows-i686"].url = browser_download_url;
|
||||
}
|
||||
// win32 signature
|
||||
if (name.endsWith("x86-setup.nsis.zip.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["windows-x86"].signature = sig;
|
||||
updateData.platforms["windows-i686"].signature = sig;
|
||||
}
|
||||
|
||||
// win arm url
|
||||
if (name.endsWith("arm64-setup.nsis.zip")) {
|
||||
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||
}
|
||||
// win arm signature
|
||||
if (name.endsWith("arm64-setup.nsis.zip.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["windows-aarch64"].signature = sig;
|
||||
}
|
||||
|
||||
// darwin url (intel)
|
||||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
||||
updateData.platforms.darwin.url = browser_download_url;
|
||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
||||
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
||||
}
|
||||
// darwin signature (intel)
|
||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.darwin.signature = sig;
|
||||
updateData.platforms["darwin-intel"].signature = sig;
|
||||
updateData.platforms["darwin-x86_64"].signature = sig;
|
||||
}
|
||||
|
||||
// darwin url (aarch)
|
||||
if (name.endsWith("aarch64.app.tar.gz")) {
|
||||
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
||||
// 使linux可以检查更新
|
||||
updateData.platforms.linux.url = browser_download_url;
|
||||
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||
updateData.platforms["linux-aarch64"].url = browser_download_url;
|
||||
updateData.platforms["linux-armv7"].url = browser_download_url;
|
||||
}
|
||||
// darwin signature (aarch)
|
||||
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["darwin-aarch64"].signature = sig;
|
||||
updateData.platforms.linux.signature = sig;
|
||||
updateData.platforms["linux-x86_64"].signature = sig;
|
||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||
updateData.platforms["linux-aarch64"].signature = sig;
|
||||
updateData.platforms["linux-armv7"].signature = sig;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
console.log(updateData);
|
||||
|
||||
// maybe should test the signature as well
|
||||
// delete the null field
|
||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||
if (!value.url) {
|
||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
||||
delete updateData.platforms[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 生成一个代理github的更新文件
|
||||
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
|
||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
||||
|
||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||
if (value.url) {
|
||||
updateDataNew.platforms[key].url =
|
||||
"https://download.clashverge.dev/" + value.url;
|
||||
} else {
|
||||
console.error(
|
||||
`Failed to get release for tag: ${tag.name}`,
|
||||
error.message,
|
||||
);
|
||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||
}
|
||||
});
|
||||
|
||||
// update the update.json
|
||||
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: UPDATE_TAG_NAME,
|
||||
});
|
||||
|
||||
// delete the old assets
|
||||
for (let asset of updateRelease.assets) {
|
||||
if (asset.name === UPDATE_JSON_FILE) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
asset_id: asset.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.name === UPDATE_JSON_PROXY) {
|
||||
await github.rest.repos
|
||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||
.catch(console.error); // do not break the pipeline
|
||||
}
|
||||
}
|
||||
|
||||
// upload new assets
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: UPDATE_JSON_FILE,
|
||||
data: JSON.stringify(updateData, null, 2),
|
||||
});
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: UPDATE_JSON_PROXY,
|
||||
data: JSON.stringify(updateDataNew, null, 2),
|
||||
});
|
||||
}
|
||||
|
||||
// get the signature file content
|
||||
|
@ -1 +0,0 @@
|
||||
avoid-breaking-exported-api = true
|
3500
src-tauri/Cargo.lock
generated
3500
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.2.3"
|
||||
version = "2.0.0"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@ -9,85 +9,72 @@ default-run = "clash-verge"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.1.0", features = [] }
|
||||
tauri-build = { version = "2.0.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3"
|
||||
anyhow = "1.0.97"
|
||||
dirs = "6.0"
|
||||
open = "5.3"
|
||||
anyhow = "1.0"
|
||||
dirs = "5.0"
|
||||
open = "5.1"
|
||||
log = "0.4"
|
||||
dunce = "1.0"
|
||||
log4rs = "1"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.40"
|
||||
sysinfo = "0.34"
|
||||
boa_engine = "0.20.0"
|
||||
chrono = "0.4"
|
||||
sysinfo = "0.32.0"
|
||||
boa_engine = "0.19.1"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
once_cell = "1.21.3"
|
||||
once_cell = "1.19"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
delay_timer = "0.11"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3.1"
|
||||
tokio = { version = "1.44", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
window-shadows = { version = "0.2.2" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] }
|
||||
regex = "1.11.1"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
||||
image = "0.25.6"
|
||||
imageproc = "0.25.0"
|
||||
tauri = { version = "2.4.0", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
"image-ico",
|
||||
"image-png",
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "main" }
|
||||
tauri = { version = "2.1.1", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
"image-ico",
|
||||
"image-png",
|
||||
] }
|
||||
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.2.0"
|
||||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-fs = "2.2.0"
|
||||
tauri-plugin-process = "2.2.0"
|
||||
tauri-plugin-clipboard-manager = "2.2.2"
|
||||
tauri-plugin-deep-link = "2.2.0"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
zip = "2.5.0"
|
||||
reqwest_dav = "0.1.15"
|
||||
network-interface = { version = "2.0.0", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.0.2"
|
||||
tauri-plugin-dialog = "2.0.2"
|
||||
tauri-plugin-fs = "2.0.2"
|
||||
tauri-plugin-notification = "2.0.1"
|
||||
tauri-plugin-process = "2.0.1"
|
||||
tauri-plugin-clipboard-manager = "2.0.1"
|
||||
tauri-plugin-deep-link = "2.0.1"
|
||||
tauri-plugin-devtools = "2.0.0-rc"
|
||||
url = "2.5.2"
|
||||
zip = "2.2.0"
|
||||
reqwest_dav = "0.1.14"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
getrandom = "0.3.2"
|
||||
tokio-tungstenite = "0.26.2"
|
||||
futures = "0.3"
|
||||
sys-locale = "0.3.2"
|
||||
async-trait = "0.1.88"
|
||||
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
||||
ab_glyph = "0.2.29"
|
||||
tungstenite = "0.26.2"
|
||||
libc = "0.2"
|
||||
getrandom = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
deelevate = "0.2.0"
|
||||
winreg = "0.55.0"
|
||||
winreg = "0.52.0"
|
||||
url = "2.5.2"
|
||||
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.2.0"
|
||||
tauri-plugin-global-shortcut = "2.2.0"
|
||||
tauri-plugin-updater = "2.6.1"
|
||||
tauri-plugin-window-state = "2.2.1"
|
||||
tauri-plugin-autostart = "2.0.0-rc"
|
||||
tauri-plugin-global-shortcut = "2.0.1"
|
||||
tauri-plugin-updater = "2.0.2"
|
||||
tauri-plugin-window-state = "2.0.2"
|
||||
#openssl
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
@ -104,34 +91,6 @@ strip = true
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
|
||||
[profile.fast-release]
|
||||
inherits = "release" # 继承 release 的配置
|
||||
panic = "abort" # 与 release 相同
|
||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||
lto = false # 禁用 LTO,提升编译速度
|
||||
opt-level = 0 # 禁用优化,大幅提升编译速度
|
||||
debug = true # 保留调试信息
|
||||
strip = false # 不剥离符号,保留调试信息
|
||||
|
||||
[profile.fast-dev]
|
||||
inherits = "dev" # 继承 dev 的配置
|
||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||
opt-level = 0 # 禁用优化,进一步提升编译速度
|
||||
incremental = true # 启用增量编译
|
||||
debug = true # 保留调试信息
|
||||
strip = false # 不剥离符号,保留调试信息
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.19.1"
|
||||
|
||||
[workspace]
|
||||
members = ["src_crates/crate_mihomo_api"]
|
||||
|
||||
# [patch.crates-io]
|
||||
# bitflags = { git = "https://github.com/bitflags/bitflags", rev = "2.9.0" }
|
||||
# zerocopy = { git = "https://github.com/google/zerocopy", rev = "v0.8.24" }
|
||||
# tungstenite = { git = "https://github.com/snapview/tungstenite-rs", rev = "v0.26.2" }
|
||||
|
Binary file not shown.
@ -6,13 +6,6 @@
|
||||
"permissions": [
|
||||
"global-shortcut:default",
|
||||
"updater:default",
|
||||
"dialog:default",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-message",
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download-and-install",
|
||||
"process:allow-restart",
|
||||
"deep-link:default",
|
||||
"window-state:default",
|
||||
"window-state:default",
|
||||
|
@ -16,12 +16,6 @@
|
||||
"identifier": "fs:scope",
|
||||
"allow": ["$APPDATA/**", "$RESOURCE/../**", "**"]
|
||||
},
|
||||
"fs:allow-app-read",
|
||||
"fs:allow-app-read-recursive",
|
||||
"fs:allow-appcache-read",
|
||||
"fs:allow-appcache-read-recursive",
|
||||
"fs:allow-appconfig-read",
|
||||
"fs:allow-appconfig-read-recursive",
|
||||
"core:window:allow-create",
|
||||
"core:window:allow-center",
|
||||
"core:window:allow-request-user-attention",
|
||||
@ -68,6 +62,7 @@
|
||||
"shell:allow-spawn",
|
||||
"shell:allow-stdin-write",
|
||||
"dialog:allow-open",
|
||||
"notification:default",
|
||||
"global-shortcut:allow-is-registered",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-register-all",
|
||||
@ -78,6 +73,7 @@
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"shell:default",
|
||||
"dialog:default"
|
||||
"dialog:default",
|
||||
"notification:default"
|
||||
]
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>io.github.clash-verge-rev.clash-verge-rev</string>
|
||||
</array>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@ -697,62 +697,25 @@ Var VC_REDIST_URL
|
||||
Var VC_REDIST_EXE
|
||||
|
||||
Section CheckAndInstallVSRuntime
|
||||
; 检查是否已安装 Visual C++ Redistributable
|
||||
${If} ${IsNativeARM64}
|
||||
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
|
||||
StrCpy $VC_REDIST_EXE "vc_redist.arm64.exe"
|
||||
|
||||
; 检查关键DLL
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
|
||||
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done
|
||||
${ElseIf} ${RunningX64}
|
||||
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
||||
StrCpy $VC_REDIST_EXE "vc_redist.x64.exe"
|
||||
|
||||
; 检查关键DLL
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
|
||||
|
||||
IfFileExists "$WINDIR\SysWOW64\msvcp140.dll" Done
|
||||
${Else}
|
||||
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x86.exe"
|
||||
StrCpy $VC_REDIST_EXE "vc_redist.x86.exe"
|
||||
|
||||
; 检查关键DLL
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done
|
||||
${EndIf}
|
||||
|
||||
checkInstall:
|
||||
; 检查注册表
|
||||
${If} ${RunningX64}
|
||||
SetRegView 64
|
||||
ReadRegDword $R0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${ARCH}" "Installed"
|
||||
${If} $R0 == "1"
|
||||
Goto Done
|
||||
${EndIf}
|
||||
${Else}
|
||||
ReadRegDword $R0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x86" "Installed"
|
||||
${If} $R0 == "1"
|
||||
Goto Done
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
; 如果没有安装,则下载并安装
|
||||
DetailPrint "正在下载 Visual C++ Redistributable..."
|
||||
; 下载并安装VC运行库
|
||||
nsisdl::download "$VC_REDIST_URL" "$TEMP\$VC_REDIST_EXE"
|
||||
Pop $0
|
||||
${If} $0 == "success"
|
||||
DetailPrint "正在安装 Visual C++ Redistributable..."
|
||||
ExecWait '"$TEMP\$VC_REDIST_EXE" /quiet /norestart' $0
|
||||
${If} $0 == 0
|
||||
DetailPrint "Visual C++ Redistributable 安装成功"
|
||||
${Else}
|
||||
DetailPrint "Visual C++ Redistributable 安装失败"
|
||||
${EndIf}
|
||||
Delete "$TEMP\$VC_REDIST_EXE"
|
||||
${Else}
|
||||
DetailPrint "Visual C++ Redistributable 下载失败"
|
||||
nsExec::Exec '"$TEMP\$VC_REDIST_EXE" /quiet /norestart'
|
||||
${EndIf}
|
||||
|
||||
Done:
|
||||
@ -765,40 +728,6 @@ Section Install
|
||||
nsExec::Exec 'netsh int tcp res'
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
${EndIf}
|
||||
|
||||
; 清理新版本的注册表项 (clash-verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "clash-verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
${EndIf}
|
||||
|
||||
; Delete old files before installation
|
||||
; Delete clash-verge.desktop
|
||||
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
|
||||
Delete "$INSTDIR\Clash Verge.exe"
|
||||
|
||||
; Copy main executable
|
||||
File "${MAINBINARYSRCPATH}"
|
||||
|
||||
@ -919,35 +848,6 @@ Section Uninstall
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
!insertmacro RemoveVergeService
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
${EndIf}
|
||||
|
||||
; 清理新版本的注册表项 (clash-verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "clash-verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
${EndIf}
|
||||
|
||||
; Delete the app directory and its content from disk
|
||||
; Copy main executable
|
||||
Delete "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
@ -962,10 +862,6 @@ Section Uninstall
|
||||
Delete "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
|
||||
; Delete clash-verge.desktop
|
||||
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
|
||||
Delete "$INSTDIR\Clash Verge.exe"
|
||||
|
||||
; Delete uninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
|
@ -1,216 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
feat, logging,
|
||||
utils::{dirs, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use tauri::Manager;
|
||||
|
||||
/// 打开应用程序所在目录
|
||||
#[tauri::command]
|
||||
pub fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
/// 打开核心所在目录
|
||||
#[tauri::command]
|
||||
pub fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
wrap_err!(open::that(core_dir))
|
||||
}
|
||||
|
||||
/// 打开日志目录
|
||||
#[tauri::command]
|
||||
pub fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||
wrap_err!(open::that(log_dir))
|
||||
}
|
||||
|
||||
/// 打开网页链接
|
||||
#[tauri::command]
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
wrap_err!(open::that(url))
|
||||
}
|
||||
|
||||
/// 打开/关闭开发者工具
|
||||
#[tauri::command]
|
||||
pub fn open_devtools(app_handle: tauri::AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if !window.is_devtools_open() {
|
||||
window.open_devtools();
|
||||
} else {
|
||||
window.close_devtools();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 退出应用
|
||||
#[tauri::command]
|
||||
pub fn exit_app() {
|
||||
feat::quit(Some(0));
|
||||
}
|
||||
|
||||
/// 重启应用
|
||||
#[tauri::command]
|
||||
pub async fn restart_app() -> CmdResult<()> {
|
||||
feat::restart_app();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取便携版标识
|
||||
#[tauri::command]
|
||||
pub fn get_portable_flag() -> CmdResult<bool> {
|
||||
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
|
||||
}
|
||||
|
||||
/// 获取应用目录
|
||||
#[tauri::command]
|
||||
pub fn get_app_dir() -> CmdResult<String> {
|
||||
let app_home_dir = wrap_err!(dirs::app_home_dir())?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
/// 获取当前自启动状态
|
||||
#[tauri::command]
|
||||
pub fn get_auto_launch_status() -> CmdResult<bool> {
|
||||
use crate::core::sysopt::Sysopt;
|
||||
wrap_err!(Sysopt::global().get_launch_status())
|
||||
}
|
||||
|
||||
/// 下载图标缓存
|
||||
#[tauri::command]
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||
let icon_path = icon_cache_dir.join(&name);
|
||||
|
||||
// 如果文件已存在,直接返回路径
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// 确保缓存目录存在
|
||||
if !icon_cache_dir.exists() {
|
||||
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||
}
|
||||
|
||||
// 使用临时文件名来下载
|
||||
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
|
||||
|
||||
// 下载文件到临时位置
|
||||
let response = wrap_err!(reqwest::get(&url).await)?;
|
||||
|
||||
// 检查内容类型是否为图片
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let is_image = content_type.starts_with("image/");
|
||||
|
||||
// 获取响应内容
|
||||
let content = wrap_err!(response.bytes().await)?;
|
||||
|
||||
// 检查内容是否为HTML (针对CDN错误页面)
|
||||
let is_html = content.len() > 15
|
||||
&& (content.starts_with(b"<!DOCTYPE html")
|
||||
|| content.starts_with(b"<html")
|
||||
|| content.starts_with(b"<?xml"));
|
||||
|
||||
// 只有当内容确实是图片时才保存
|
||||
if is_image && !is_html {
|
||||
{
|
||||
let mut file = match std::fs::File::create(&temp_path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
} else {
|
||||
return Err("Failed to create temporary file".into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||
}
|
||||
|
||||
// 再次检查目标文件是否已存在,避免重命名覆盖其他线程已完成的文件
|
||||
if !icon_path.exists() {
|
||||
match std::fs::rename(&temp_path, &icon_path) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
}
|
||||
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
Err(format!("下载的内容不是有效图片: {}", url))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct IconInfo {
|
||||
name: String,
|
||||
previous_t: String,
|
||||
current_t: String,
|
||||
}
|
||||
|
||||
/// 复制图标文件
|
||||
#[tauri::command]
|
||||
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
use std::{fs, path::Path};
|
||||
|
||||
let file_path = Path::new(&path);
|
||||
|
||||
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
|
||||
if !icon_dir.exists() {
|
||||
let _ = fs::create_dir_all(&icon_dir);
|
||||
}
|
||||
let ext = match file_path.extension() {
|
||||
Some(e) => e.to_string_lossy().to_string(),
|
||||
None => "ico".to_string(),
|
||||
};
|
||||
|
||||
let dest_path = icon_dir.join(format!(
|
||||
"{0}-{1}.{ext}",
|
||||
icon_info.name, icon_info.current_t
|
||||
));
|
||||
if file_path.exists() {
|
||||
if icon_info.previous_t.trim() != "" {
|
||||
fs::remove_file(
|
||||
icon_dir.join(format!("{0}-{1}.png", icon_info.name, icon_info.previous_t)),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
fs::remove_file(
|
||||
icon_dir.join(format!("{0}-{1}.ico", icon_info.name, icon_info.previous_t)),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"Copying icon file path: {:?} -> file dist: {:?}",
|
||||
path,
|
||||
dest_path
|
||||
);
|
||||
match fs::copy(file_path, &dest_path) {
|
||||
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
} else {
|
||||
Err("file not found".to_string())
|
||||
}
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core::*, feat, module::mihomo::MihomoManager, wrap_err};
|
||||
use serde_yaml::Mapping;
|
||||
|
||||
/// 复制Clash环境变量
|
||||
#[tauri::command]
|
||||
pub fn copy_clash_env() -> CmdResult {
|
||||
feat::copy_clash_env();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取Clash信息
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest().get_client_info())
|
||||
}
|
||||
|
||||
/// 修改Clash配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
wrap_err!(feat::patch_clash(payload).await)
|
||||
}
|
||||
|
||||
/// 修改Clash模式
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
feat::change_clash_mode(payload);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 切换Clash核心
|
||||
#[tauri::command]
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||
log::info!(target: "app", "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global()
|
||||
.change_core(Some(clash_core.clone()))
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "core changed to {clash_core}");
|
||||
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = err.to_string();
|
||||
log::error!(target: "app", "failed to change core: {error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 重启核心
|
||||
#[tauri::command]
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().restart_core().await)
|
||||
}
|
||||
|
||||
/// 获取代理延迟
|
||||
#[tauri::command]
|
||||
pub async fn clash_api_get_proxy_delay(
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> CmdResult<serde_json::Value> {
|
||||
MihomoManager::global()
|
||||
.test_proxy_delay(&name, url, timeout)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 测试URL延迟
|
||||
#[tauri::command]
|
||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
||||
}
|
||||
|
||||
/// 保存DNS配置到单独文件
|
||||
#[tauri::command]
|
||||
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
use crate::utils::dirs;
|
||||
use serde_yaml;
|
||||
use std::fs;
|
||||
|
||||
// 获取DNS配置文件路径
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
// 保存DNS配置到文件
|
||||
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
||||
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
|
||||
log::info!(target: "app", "DNS config saved to {:?}", dns_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 应用或撤销DNS配置
|
||||
#[tauri::command]
|
||||
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, CoreManager},
|
||||
utils::dirs,
|
||||
};
|
||||
use tauri::async_runtime;
|
||||
|
||||
// 使用spawn来处理异步操作
|
||||
async_runtime::spawn(async move {
|
||||
if apply {
|
||||
// 读取DNS配置文件
|
||||
let dns_path = match dirs::app_home_dir() {
|
||||
Ok(path) => path.join("dns_config.yaml"),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get home dir: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !dns_path.exists() {
|
||||
log::warn!(target: "app", "DNS config file not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to read DNS config: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 解析DNS配置并创建patch
|
||||
let patch_config = match serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
||||
Ok(config) => {
|
||||
let mut patch = serde_yaml::Mapping::new();
|
||||
patch.insert("dns".into(), config.into());
|
||||
patch
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to parse DNS config: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(target: "app", "Applying DNS config from file");
|
||||
|
||||
// 重新生成配置,确保DNS配置被正确应用
|
||||
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
|
||||
Config::runtime()
|
||||
.latest()
|
||||
.patch_config(patch_config.clone());
|
||||
|
||||
// 首先重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 然后应用新配置
|
||||
if let Err(err) = CoreManager::global().update_config().await {
|
||||
log::error!(target: "app", "Failed to apply config with DNS: {}", err);
|
||||
} else {
|
||||
log::info!(target: "app", "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
} else {
|
||||
// 当关闭DNS设置时,不需要对配置进行任何修改
|
||||
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
|
||||
log::info!(target: "app", "DNS settings disabled, regenerating config");
|
||||
|
||||
// 重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config: {}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用新配置
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "Config regenerated successfully");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查DNS配置文件是否存在
|
||||
#[tauri::command]
|
||||
pub fn check_dns_config_exists() -> CmdResult<bool> {
|
||||
use crate::utils::dirs;
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
Ok(dns_path.exists())
|
||||
}
|
||||
|
||||
/// 获取DNS配置文件内容
|
||||
#[tauri::command]
|
||||
pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||
use crate::utils::dirs;
|
||||
use std::fs;
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
if !dns_path.exists() {
|
||||
return Err("DNS config file not found".into());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
|
||||
Ok(content)
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
use crate::module::lightweight;
|
||||
|
||||
use super::CmdResult;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||
lightweight::entry_lightweight_mode();
|
||||
Ok(())
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,38 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
// Common result type used by command functions
|
||||
pub type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
// Command modules
|
||||
pub mod app;
|
||||
pub mod clash;
|
||||
pub mod lightweight;
|
||||
pub mod media_unlock_checker;
|
||||
pub mod network;
|
||||
pub mod profile;
|
||||
pub mod proxy;
|
||||
pub mod runtime;
|
||||
pub mod save_profile;
|
||||
pub mod service;
|
||||
pub mod system;
|
||||
pub mod uwp;
|
||||
pub mod validate;
|
||||
pub mod verge;
|
||||
pub mod webdav;
|
||||
|
||||
// Re-export all command functions for backwards compatibility
|
||||
pub use app::*;
|
||||
pub use clash::*;
|
||||
pub use lightweight::*;
|
||||
pub use media_unlock_checker::*;
|
||||
pub use network::*;
|
||||
pub use profile::*;
|
||||
pub use proxy::*;
|
||||
pub use runtime::*;
|
||||
pub use save_profile::*;
|
||||
pub use service::*;
|
||||
pub use system::*;
|
||||
pub use uwp::*;
|
||||
pub use validate::*;
|
||||
pub use verge::*;
|
||||
pub use webdav::*;
|
@ -1,63 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::wrap_err;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml::Mapping;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert(
|
||||
"server".into(),
|
||||
format!("{}:{}", current.host, current.port).into(),
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Autoproxy::get_auto_proxy())?;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// 获取网络接口列表
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces() -> Vec<String> {
|
||||
use sysinfo::Networks;
|
||||
let mut result = Vec::new();
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
for (interface_name, _) in &networks {
|
||||
result.push(interface_name.clone());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 获取网络接口详细信息
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
|
||||
|
||||
let names = get_network_interfaces();
|
||||
let interfaces = wrap_err!(NetworkInterface::show())?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for interface in interfaces {
|
||||
if names.contains(&interface.name) {
|
||||
result.push(interface);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
@ -1,244 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
config::{Config, IProfiles, PrfItem, PrfOption},
|
||||
core::{handle, tray::Tray, CoreManager},
|
||||
feat, logging, ret_err,
|
||||
utils::{dirs, help, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
|
||||
/// 获取配置文件列表
|
||||
#[tauri::command]
|
||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||
let _ = Tray::global().update_menu();
|
||||
Ok(Config::profiles().data().clone())
|
||||
}
|
||||
|
||||
/// 增强配置文件
|
||||
#[tauri::command]
|
||||
pub async fn enhance_profiles() -> CmdResult {
|
||||
wrap_err!(feat::enhance_profiles().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 导入配置文件
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
/// 重新排序配置文件
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||
}
|
||||
|
||||
/// 创建配置文件
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
/// 更新配置文件
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
wrap_err!(feat::update_profile(index, option).await)
|
||||
}
|
||||
|
||||
/// 删除配置文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||
if should_update {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
logging!(info, Type::Cmd, true, "开始修改配置文件");
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().latest().current.clone();
|
||||
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
|
||||
|
||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||
if let Some(new_profile) = profiles.current.as_ref() {
|
||||
if current_profile.as_ref() != Some(new_profile) {
|
||||
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
|
||||
|
||||
// 获取目标配置文件路径
|
||||
let profiles_config = Config::profiles();
|
||||
let profiles_data = profiles_config.latest();
|
||||
let config_file_result = match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
if let Some(file) = &item.file {
|
||||
let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
|
||||
path.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// 如果获取到文件路径,检查YAML语法
|
||||
if let Some(file_path) = config_file_result {
|
||||
if !file_path.exists() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"目标配置文件不存在: {}",
|
||||
file_path.display()
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_not_found",
|
||||
format!("{}", file_path.display()),
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match std::fs::read_to_string(&file_path) {
|
||||
Ok(content) => match serde_yaml::from_str::<serde_yaml::Value>(&content) {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!(" {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"目标配置文件存在YAML语法错误:{}",
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
&error_msg,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {}", err);
|
||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_error",
|
||||
&error_msg,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新profiles配置
|
||||
logging!(info, Type::Cmd, true, "正在更新配置草稿");
|
||||
let _ = Config::profiles().draft().patch_config(profiles);
|
||||
|
||||
// 更新配置并进行验证
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Cmd, true, "配置更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
let _ = Tray::global().update_tooltip();
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
Ok(true)
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().discard();
|
||||
// 如果验证失败,恢复到之前的配置
|
||||
if let Some(prev_profile) = current_profile {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"尝试恢复到之前的配置: {}",
|
||||
prev_profile
|
||||
);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
// 静默恢复,不触发验证
|
||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
|
||||
}
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
|
||||
Config::profiles().discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config_by_profile_index(
|
||||
_app_handle: tauri::AppHandle,
|
||||
profile_index: String,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
|
||||
|
||||
let profiles = IProfiles {
|
||||
current: Some(profile_index),
|
||||
items: None,
|
||||
};
|
||||
patch_profiles_config(profiles).await
|
||||
}
|
||||
|
||||
/// 修改某个profile item的
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 查看配置文件
|
||||
#[tauri::command]
|
||||
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
let file = {
|
||||
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
}?;
|
||||
|
||||
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
}
|
||||
|
||||
wrap_err!(help::open_file(app_handle, path))
|
||||
}
|
||||
|
||||
/// 读取配置文件内容
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::module::mihomo::MihomoManager;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
let mannager = MihomoManager::global();
|
||||
|
||||
mannager
|
||||
.refresh_proxies()
|
||||
.await
|
||||
.map(|_| mannager.get_proxies())
|
||||
.or_else(|_| Ok(mannager.get_proxies()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
let mannager = MihomoManager::global();
|
||||
|
||||
mannager
|
||||
.refresh_providers_proxies()
|
||||
.await
|
||||
.map(|_| mannager.get_providers_proxies())
|
||||
.or_else(|_| Ok(mannager.get_providers_proxies()))
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, wrap_err};
|
||||
use anyhow::Context;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 获取运行时配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest().config.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时YAML配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(
|
||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||
))
|
||||
}
|
||||
|
||||
/// 获取运行时存在的键
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest().exists_keys.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时日志
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest().chain_logs.clone())
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core::*, utils::dirs, wrap_err};
|
||||
use std::fs;
|
||||
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 在异步操作前完成所有文件操作
|
||||
let (file_path, original_content, is_merge_file) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles_guard = profiles.latest();
|
||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||
// 确定是否为merge类型文件
|
||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||
let content = wrap_err!(item.read_file())?;
|
||||
let path = item.file.clone().ok_or("file field is null")?;
|
||||
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||
(profiles_dir.join(path), content, is_merge)
|
||||
};
|
||||
|
||||
// 保存新的配置文件
|
||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
println!(
|
||||
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
|
||||
file_path_str, is_merge_file
|
||||
);
|
||||
|
||||
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||
if is_merge_file {
|
||||
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, Some(true))
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] merge文件语法验证通过");
|
||||
// 成功后尝试更新整体配置
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
|
||||
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
// 发送合并文件专用错误通知
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非merge文件使用完整验证流程
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, None)
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] 验证成功");
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] 验证失败: {}", error_msg);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
|
||||
// 智能判断错误类型
|
||||
let is_script_error = file_path_str.ends_with(".js")
|
||||
|| error_msg.contains("Script syntax error")
|
||||
|| error_msg.contains("Script must contain a main function")
|
||||
|| error_msg.contains("Failed to read script file");
|
||||
|
||||
if error_msg.contains("YAML syntax error")
|
||||
|| error_msg.contains("Failed to read file:")
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||
{
|
||||
// 普通YAML错误使用YAML通知处理
|
||||
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||
} else if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
println!("[cmd配置save] 脚本文件验证失败,发送通知");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
// 普通配置错误使用一般通知
|
||||
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{service, CoreManager},
|
||||
utils::i18n::t,
|
||||
};
|
||||
|
||||
async fn execute_service_operation(
|
||||
service_op: impl std::future::Future<Output = Result<(), impl ToString + std::fmt::Debug>>,
|
||||
op_type: &str,
|
||||
) -> CmdResult {
|
||||
if service_op.await.is_err() {
|
||||
let emsg = format!("{} {} failed", op_type, "Service");
|
||||
return Err(t(emsg.as_str()));
|
||||
}
|
||||
if CoreManager::global().restart_core().await.is_err() {
|
||||
let emsg = format!("{} {} failed", "Restart", "Core");
|
||||
return Err(t(emsg.as_str()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> CmdResult {
|
||||
execute_service_operation(service::install_service(), "Install").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> CmdResult {
|
||||
execute_service_operation(service::uninstall_service(), "Uninstall").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reinstall_service() -> CmdResult {
|
||||
execute_service_operation(service::reinstall_service(), "Reinstall").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn repair_service() -> CmdResult {
|
||||
execute_service_operation(service::force_reinstall_service(), "Repair").await
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{handle, CoreManager},
|
||||
module::sysinfo::PlatformSpecification,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
sync::atomic::{AtomicI64, Ordering},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
// 存储应用启动时间的全局变量
|
||||
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||
// 获取当前系统时间,转换为毫秒级时间戳
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
AtomicI64::new(now)
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cliboard = app_handle.clipboard();
|
||||
if cliboard.write_text(info).is_err() {
|
||||
log::error!(target: "app", "Failed to write to clipboard");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_system_info() -> CmdResult<String> {
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// 获取当前内核运行模式
|
||||
#[tauri::command]
|
||||
pub async fn get_running_mode() -> Result<String, String> {
|
||||
Ok(CoreManager::global().get_running_mode().await.to_string())
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[tauri::command]
|
||||
pub fn get_app_uptime() -> CmdResult<i64> {
|
||||
let start_time = APP_START_TIME.load(Ordering::Relaxed);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
Ok(now - start_time)
|
||||
}
|
||||
|
||||
/// 检查应用是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
|
||||
let result = Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 非Windows平台检测是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
use super::CmdResult;
|
||||
|
||||
/// Platform-specific implementation for UWP functionality
|
||||
#[cfg(windows)]
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
use crate::{core::win_uwp, wrap_err};
|
||||
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub implementation for non-Windows platforms
|
||||
#[cfg(not(windows))]
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Command exposed to Tauri
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
platform::invoke_uwp_tool().await
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::core::*;
|
||||
|
||||
/// 发送脚本验证通知消息
|
||||
#[tauri::command]
|
||||
pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
|
||||
handle::Handle::notice_message(&status, &msg);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 处理脚本验证相关的所有消息通知
|
||||
/// 统一通知接口,保持消息类型一致性
|
||||
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
if !result.0 {
|
||||
let error_msg = &result.1;
|
||||
|
||||
// 根据错误消息内容判断错误类型
|
||||
let status = if error_msg.starts_with("File not found:") {
|
||||
"config_validate::file_not_found"
|
||||
} else if error_msg.starts_with("Failed to read script file:") {
|
||||
"config_validate::script_error"
|
||||
} else if error_msg.starts_with("Script syntax error:") {
|
||||
"config_validate::script_syntax_error"
|
||||
} else if error_msg == "Script must contain a main function" {
|
||||
"config_validate::script_missing_main"
|
||||
} else {
|
||||
// 如果是其他类型错误,作为一般脚本错误处理
|
||||
"config_validate::script_error"
|
||||
};
|
||||
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证指定脚本文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
||||
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path, None)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
handle_script_validation_notice(&result, "脚本文件");
|
||||
Ok(result.0) // 返回验证结果布尔值
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理YAML验证相关的所有消息通知
|
||||
/// 统一通知接口,保持消息类型一致性
|
||||
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
if !result.0 {
|
||||
let error_msg = &result.1;
|
||||
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
|
||||
|
||||
// 检查是否为merge文件
|
||||
let is_merge_file = file_type.contains("合并");
|
||||
|
||||
// 根据错误消息内容判断错误类型
|
||||
let status = if error_msg.starts_with("File not found:") {
|
||||
"config_validate::file_not_found"
|
||||
} else if error_msg.starts_with("Failed to read file:") {
|
||||
"config_validate::yaml_read_error"
|
||||
} else if error_msg.starts_with("YAML syntax error:") {
|
||||
if is_merge_file {
|
||||
"config_validate::merge_syntax_error"
|
||||
} else {
|
||||
"config_validate::yaml_syntax_error"
|
||||
}
|
||||
} else if error_msg.contains("mapping values are not allowed") {
|
||||
if is_merge_file {
|
||||
"config_validate::merge_mapping_error"
|
||||
} else {
|
||||
"config_validate::yaml_mapping_error"
|
||||
}
|
||||
} else if error_msg.contains("did not find expected key") {
|
||||
if is_merge_file {
|
||||
"config_validate::merge_key_error"
|
||||
} else {
|
||||
"config_validate::yaml_key_error"
|
||||
}
|
||||
} else {
|
||||
// 如果是其他类型错误,根据文件类型作为一般错误处理
|
||||
if is_merge_file {
|
||||
"config_validate::merge_error"
|
||||
} else {
|
||||
"config_validate::yaml_error"
|
||||
}
|
||||
};
|
||||
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, feat, wrap_err};
|
||||
|
||||
/// 获取Verge配置
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
Ok(IVergeResponse::from(verge_data))
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
wrap_err!(feat::patch_verge(payload, false).await)
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core, feat, wrap_err};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
|
||||
/// 保存 WebDAV 配置
|
||||
#[tauri::command]
|
||||
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
||||
let patch = IVerge {
|
||||
webdav_url: Some(url),
|
||||
webdav_username: Some(username),
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().apply();
|
||||
Config::verge()
|
||||
.data()
|
||||
.save_file()
|
||||
.map_err(|err| err.to_string())?;
|
||||
core::backup::WebDavClient::global().reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 创建 WebDAV 备份并上传
|
||||
#[tauri::command]
|
||||
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||
wrap_err!(feat::create_backup_and_upload_webdav().await)
|
||||
}
|
||||
|
||||
/// 列出 WebDAV 上的备份文件
|
||||
#[tauri::command]
|
||||
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
||||
wrap_err!(feat::list_wevdav_backup().await)
|
||||
}
|
||||
|
||||
/// 删除 WebDAV 上的备份文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::delete_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
/// 从 WebDAV 恢复备份文件
|
||||
#[tauri::command]
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::restore_webdav_backup(filename).await)
|
||||
}
|
428
src-tauri/src/cmds.rs
Normal file
428
src-tauri/src/cmds.rs
Normal file
@ -0,0 +1,428 @@
|
||||
use crate::{
|
||||
config::*,
|
||||
core::*,
|
||||
feat,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use crate::{ret_err, wrap_err};
|
||||
use anyhow::{Context, Result};
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
type CmdResult<T = ()> = Result<T, String>;
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn copy_clash_env() -> CmdResult {
|
||||
feat::copy_clash_env();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||
Ok(Config::profiles().data().clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enhance_profiles() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
wrap_err!(feat::update_profile(index, option).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||
if should_update {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 修改profiles的
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
let _ = handle::Handle::update_systray_part();
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::profiles().discard();
|
||||
log::error!(target: "app", "{err}");
|
||||
Err(format!("{err}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改某个profile item的
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||
wrap_err!(timer::Timer::global().refresh())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
let file = {
|
||||
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
}?;
|
||||
|
||||
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
}
|
||||
|
||||
wrap_err!(help::open_file(app_handle, path))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
wrap_err!(item.save_file(file_data.unwrap()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest().get_client_info())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest().config.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(
|
||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest().exists_keys.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest().chain_logs.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
wrap_err!(feat::patch_clash(payload).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
Ok(IVergeResponse::from(verge_data))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
wrap_err!(feat::patch_verge(payload).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
|
||||
wrap_err!(CoreManager::global().change_core(clash_core).await)
|
||||
}
|
||||
|
||||
/// restart the sidecar
|
||||
#[tauri::command]
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().restart_core().await)
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert(
|
||||
"server".into(),
|
||||
format!("{}:{}", current.host, current.port).into(),
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Autoproxy::get_auto_proxy())?;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
wrap_err!(open::that(core_dir))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||
wrap_err!(open::that(log_dir))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
wrap_err!(open::that(url))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod uwp {
|
||||
use super::*;
|
||||
use crate::core::win_uwp;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clash_api_get_proxy_delay(
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> CmdResult<clash_api::DelayRes> {
|
||||
match clash_api::get_proxy_delay(name, url, timeout).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_portable_flag() -> CmdResult<bool> {
|
||||
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_dir() -> CmdResult<String> {
|
||||
let app_home_dir = wrap_err!(dirs::app_home_dir())?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||
let icon_path = icon_cache_dir.join(name);
|
||||
if !icon_cache_dir.exists() {
|
||||
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||
}
|
||||
if !icon_path.exists() {
|
||||
let response = wrap_err!(reqwest::get(url).await)?;
|
||||
|
||||
let mut file = wrap_err!(std::fs::File::create(&icon_path))?;
|
||||
|
||||
let content = wrap_err!(response.bytes().await)?;
|
||||
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||
}
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
|
||||
let file_path = std::path::Path::new(&path);
|
||||
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
|
||||
if !icon_dir.exists() {
|
||||
let _ = std::fs::create_dir_all(&icon_dir);
|
||||
}
|
||||
let ext = match file_path.extension() {
|
||||
Some(e) => e.to_string_lossy().to_string(),
|
||||
None => "ico".to_string(),
|
||||
};
|
||||
|
||||
let png_dest_path = icon_dir.join(format!("{name}.png"));
|
||||
let ico_dest_path = icon_dir.join(format!("{name}.ico"));
|
||||
let dest_path = icon_dir.join(format!("{name}.{ext}"));
|
||||
if file_path.exists() {
|
||||
std::fs::remove_file(png_dest_path).unwrap_or_default();
|
||||
std::fs::remove_file(ico_dest_path).unwrap_or_default();
|
||||
match std::fs::copy(file_path, &dest_path) {
|
||||
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
} else {
|
||||
Err("file not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces() -> Vec<String> {
|
||||
use sysinfo::Networks;
|
||||
let mut result = Vec::new();
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
for (interface_name, _) in &networks {
|
||||
result.push(interface_name.clone());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||
use network_interface::NetworkInterface;
|
||||
use network_interface::NetworkInterfaceConfig;
|
||||
|
||||
let names = get_network_interfaces();
|
||||
let interfaces = wrap_err!(NetworkInterface::show())?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for interface in interfaces {
|
||||
if names.contains(&interface.name) {
|
||||
result.push(interface);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_devtools(app_handle: tauri::AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if !window.is_devtools_open() {
|
||||
window.open_devtools();
|
||||
} else {
|
||||
window.close_devtools();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn exit_app() {
|
||||
feat::quit(Some(0));
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
||||
let patch = IVerge {
|
||||
webdav_url: Some(url),
|
||||
webdav_username: Some(username),
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().apply();
|
||||
Config::verge()
|
||||
.data()
|
||||
.save_file()
|
||||
.map_err(|err| err.to_string())?;
|
||||
backup::WebDavClient::global().reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||
wrap_err!(feat::create_backup_and_upload_webdav().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
||||
wrap_err!(feat::list_wevdav_backup().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::delete_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::restore_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restart_app() -> CmdResult<()> {
|
||||
feat::restart_app();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub mod uwp {
|
||||
use super::*;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -38,7 +38,6 @@ impl IClashTemp {
|
||||
tun.insert("strict-route".into(), false.into());
|
||||
tun.insert("auto-detect-interface".into(), true.into());
|
||||
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
map.insert("redir-port".into(), 7895.into());
|
||||
#[cfg(target_os = "linux")]
|
||||
@ -157,20 +156,17 @@ impl IClashTemp {
|
||||
}
|
||||
|
||||
pub fn guard_mixed_port(config: &Mapping) -> u16 {
|
||||
let raw_value = config.get("mixed-port");
|
||||
|
||||
let mut port = raw_value
|
||||
let mut port = config
|
||||
.get("mixed-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7897);
|
||||
|
||||
if port == 0 {
|
||||
port = 7897;
|
||||
}
|
||||
|
||||
port
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use crate::{
|
||||
config::PrfItem,
|
||||
core::{handle, CoreManager},
|
||||
enhance, logging,
|
||||
utils::{dirs, help, logging::Type},
|
||||
enhance,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::path::PathBuf;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
@ -66,63 +64,20 @@ impl Config {
|
||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||
Self::profiles().data().append_item(script_item.clone())?;
|
||||
}
|
||||
// 生成运行时配置
|
||||
if let Err(err) = Self::generate().await {
|
||||
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "生成运行时配置成功");
|
||||
}
|
||||
crate::log_err!(Self::generate().await);
|
||||
if let Err(err) = Self::generate_file(ConfigType::Run) {
|
||||
log::error!(target: "app", "{err}");
|
||||
|
||||
// 生成运行时配置文件并验证
|
||||
let config_result = Self::generate_file(ConfigType::Run);
|
||||
|
||||
let validation_result = if config_result.is_ok() {
|
||||
// 验证配置文件
|
||||
logging!(info, Type::Config, true, "开始验证配置");
|
||||
|
||||
match CoreManager::global().validate_config().await {
|
||||
Ok((is_valid, error_msg)) => {
|
||||
if !is_valid {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
||||
error_msg
|
||||
);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::boot_error", &error_msg)
|
||||
.await?;
|
||||
Some(("config_validate::boot_error", error_msg))
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "配置验证成功");
|
||||
Some(("config_validate::success", String::new()))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::process_terminated", "")
|
||||
.await?;
|
||||
Some(("config_validate::process_terminated", String::new()))
|
||||
}
|
||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||
// 如果不存在就将默认的clash文件拿过来
|
||||
if !runtime_path.exists() {
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::error", "")
|
||||
.await?;
|
||||
Some(("config_validate::error", String::new()))
|
||||
};
|
||||
|
||||
// 在单独的任务中发送通知
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
handle::Handle::notice_message(msg_type, &msg_content);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
|
||||
// Generate random nonce
|
||||
let mut nonce = vec![0u8; NONCE_LENGTH];
|
||||
getrandom::fill(&mut nonce)?;
|
||||
getrandom::getrandom(&mut nonce)?;
|
||||
|
||||
// Encrypt data
|
||||
let ciphertext = cipher
|
||||
|
@ -8,9 +8,14 @@ mod profiles;
|
||||
mod runtime;
|
||||
mod verge;
|
||||
|
||||
pub use self::{
|
||||
clash::*, config::*, draft::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*,
|
||||
};
|
||||
pub use self::clash::*;
|
||||
pub use self::config::*;
|
||||
pub use self::draft::*;
|
||||
pub use self::encrypt::*;
|
||||
pub use self::prfitem::*;
|
||||
pub use self::profiles::*;
|
||||
pub use self::runtime::*;
|
||||
pub use self::verge::*;
|
||||
|
||||
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
||||
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||
|
@ -234,10 +234,10 @@ impl PrfItem {
|
||||
option: Option<PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
let opt_ref = option.as_ref();
|
||||
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
||||
let accept_invalid_certs =
|
||||
opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||
opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||
|
@ -71,6 +71,7 @@ impl IProfiles {
|
||||
if let Some(current) = patch.current {
|
||||
let items = self.items.as_ref().unwrap();
|
||||
let some_uid = Some(current);
|
||||
|
||||
if items.iter().any(|e| e.uid == some_uid) {
|
||||
self.current = some_uid;
|
||||
}
|
||||
@ -464,25 +465,4 @@ impl IProfiles {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断profile是否是current指向的
|
||||
pub fn is_current_profile_index(&self, index: String) -> bool {
|
||||
self.current == Some(index)
|
||||
}
|
||||
|
||||
/// 获取所有的profiles(uid,名称)
|
||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
||||
self.items.as_ref().map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
||||
Some((uid, name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
|
||||
utils::{dirs, help, i18n},
|
||||
};
|
||||
use crate::config::DEFAULT_PAC;
|
||||
use crate::config::{deserialize_encrypted, serialize_encrypted};
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -70,9 +69,6 @@ pub struct IVerge {
|
||||
/// enable proxy guard
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
|
||||
/// enable dns settings - this controls whether dns_config.yaml is applied
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
|
||||
/// always use default bypass
|
||||
pub use_default_bypass: Option<bool>,
|
||||
|
||||
@ -102,13 +98,6 @@ pub struct IVerge {
|
||||
/// format: {func},{key}
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
|
||||
/// enable global hotkey
|
||||
pub enable_global_hotkey: Option<bool>,
|
||||
|
||||
/// 首页卡片设置
|
||||
/// 控制首页各个卡片的显示和隐藏
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
|
||||
/// 切换代理时自动关闭连接
|
||||
pub auto_close_connection: Option<bool>,
|
||||
|
||||
@ -186,19 +175,6 @@ pub struct IVerge {
|
||||
default
|
||||
)]
|
||||
pub webdav_password: Option<String>,
|
||||
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
|
||||
/// 自动进入轻量模式
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
|
||||
/// 自动进入轻量模式的延迟(分钟)
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
|
||||
/// 服务状态跟踪
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@ -226,21 +202,6 @@ pub struct IVergeTheme {
|
||||
}
|
||||
|
||||
impl IVerge {
|
||||
fn get_system_language() -> String {
|
||||
let sys_lang = sys_locale::get_locale()
|
||||
.unwrap_or_else(|| String::from("en"))
|
||||
.to_lowercase();
|
||||
|
||||
let lang_code = sys_lang.split(['_', '-']).next().unwrap_or("en");
|
||||
let supported_languages = i18n::get_supported_languages();
|
||||
|
||||
if supported_languages.contains(&lang_code.to_string()) {
|
||||
lang_code.to_string()
|
||||
} else {
|
||||
String::from("en")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||
Ok(config) => config,
|
||||
@ -254,13 +215,13 @@ impl IVerge {
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
clash_core: Some("verge-mihomo".into()),
|
||||
language: Some(Self::get_system_language()),
|
||||
language: Some("zh".into()),
|
||||
theme_mode: Some("system".into()),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
env_type: Some("bash".into()),
|
||||
#[cfg(target_os = "windows")]
|
||||
env_type: Some("powershell".into()),
|
||||
start_page: Some("/home".into()),
|
||||
start_page: Some("/".into()),
|
||||
traffic_graph: Some(true),
|
||||
enable_memory_usage: Some(true),
|
||||
enable_group_icon: Some(true),
|
||||
@ -299,14 +260,6 @@ impl IVerge {
|
||||
webdav_url: None,
|
||||
webdav_username: None,
|
||||
webdav_password: None,
|
||||
enable_tray_speed: Some(true),
|
||||
enable_tray_icon: Some(true),
|
||||
enable_global_hotkey: Some(true),
|
||||
enable_auto_light_weight_mode: Some(false),
|
||||
auto_light_weight_minutes: Some(10),
|
||||
enable_dns_settings: Some(true),
|
||||
home_cards: None,
|
||||
service_state: None,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
@ -373,7 +326,6 @@ impl IVerge {
|
||||
patch!(web_ui_list);
|
||||
patch!(clash_core);
|
||||
patch!(hotkeys);
|
||||
patch!(enable_global_hotkey);
|
||||
|
||||
patch!(auto_close_connection);
|
||||
patch!(auto_check_update);
|
||||
@ -387,13 +339,6 @@ impl IVerge {
|
||||
patch!(webdav_url);
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
patch!(enable_tray_speed);
|
||||
patch!(enable_tray_icon);
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
patch!(enable_dns_settings);
|
||||
patch!(home_cards);
|
||||
patch!(service_state);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
@ -446,7 +391,6 @@ pub struct IVergeResponse {
|
||||
pub enable_silent_start: Option<bool>,
|
||||
pub enable_system_proxy: Option<bool>,
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
pub enable_global_hotkey: Option<bool>,
|
||||
pub use_default_bypass: Option<bool>,
|
||||
pub system_proxy_bypass: Option<String>,
|
||||
pub proxy_guard_duration: Option<u64>,
|
||||
@ -481,13 +425,6 @@ pub struct IVergeResponse {
|
||||
pub webdav_url: Option<String>,
|
||||
pub webdav_username: Option<String>,
|
||||
pub webdav_password: Option<String>,
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
impl From<IVerge> for IVergeResponse {
|
||||
@ -514,7 +451,6 @@ impl From<IVerge> for IVergeResponse {
|
||||
enable_silent_start: verge.enable_silent_start,
|
||||
enable_system_proxy: verge.enable_system_proxy,
|
||||
enable_proxy_guard: verge.enable_proxy_guard,
|
||||
enable_global_hotkey: verge.enable_global_hotkey,
|
||||
use_default_bypass: verge.use_default_bypass,
|
||||
system_proxy_bypass: verge.system_proxy_bypass,
|
||||
proxy_guard_duration: verge.proxy_guard_duration,
|
||||
@ -549,13 +485,6 @@ impl From<IVerge> for IVergeResponse {
|
||||
webdav_url: verge.webdav_url,
|
||||
webdav_username: verge.webdav_username,
|
||||
webdav_password: verge.webdav_password,
|
||||
enable_tray_speed: verge.enable_tray_speed,
|
||||
enable_tray_icon: verge.enable_tray_icon,
|
||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
enable_dns_settings: verge.enable_dns_settings,
|
||||
home_cards: verge.home_cards,
|
||||
service_state: verge.service_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,188 +1,117 @@
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use crate::config::Config;
|
||||
use crate::utils::dirs;
|
||||
use anyhow::Error;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::{consts::OS, temp_dir},
|
||||
fs,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
use std::env::{consts::OS, temp_dir};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
const TIMEOUT_UPLOAD: u64 = 300; // 上传超时 5 分钟
|
||||
const TIMEOUT_DOWNLOAD: u64 = 300; // 下载超时 5 分钟
|
||||
const TIMEOUT_LIST: u64 = 3; // 列表超时 30 秒
|
||||
const TIMEOUT_DELETE: u64 = 3; // 删除超时 30 秒
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WebDavConfig {
|
||||
url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
|
||||
enum Operation {
|
||||
Upload,
|
||||
Download,
|
||||
List,
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
fn timeout(&self) -> u64 {
|
||||
match self {
|
||||
Operation::Upload => TIMEOUT_UPLOAD,
|
||||
Operation::Download => TIMEOUT_DOWNLOAD,
|
||||
Operation::List => TIMEOUT_LIST,
|
||||
Operation::Delete => TIMEOUT_DELETE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebDavClient {
|
||||
config: Arc<Mutex<Option<WebDavConfig>>>,
|
||||
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
|
||||
client: Arc<Mutex<Option<reqwest_dav::Client>>>,
|
||||
}
|
||||
|
||||
impl WebDavClient {
|
||||
pub fn global() -> &'static WebDavClient {
|
||||
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
||||
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
|
||||
config: Arc::new(Mutex::new(None)),
|
||||
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
client: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
|
||||
// 先尝试从缓存获取
|
||||
{
|
||||
let clients = self.clients.lock();
|
||||
if let Some(client) = clients.get(&op) {
|
||||
return Ok(client.clone());
|
||||
async fn get_client(&self) -> Result<reqwest_dav::Client, Error> {
|
||||
if self.client.lock().is_none() {
|
||||
let verge = Config::verge().latest().clone();
|
||||
if verge.webdav_url.is_none()
|
||||
|| verge.webdav_username.is_none()
|
||||
|| verge.webdav_password.is_none()
|
||||
{
|
||||
let msg =
|
||||
"Unable to create web dav client, please make sure the webdav config is correct"
|
||||
.to_string();
|
||||
log::error!(target: "app","{}",msg);
|
||||
return Err(anyhow::Error::msg(msg));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取或创建配置
|
||||
let config = {
|
||||
let mut lock = self.config.lock();
|
||||
if let Some(cfg) = lock.as_ref() {
|
||||
cfg.clone()
|
||||
} else {
|
||||
let verge = Config::verge().latest().clone();
|
||||
if verge.webdav_url.is_none()
|
||||
|| verge.webdav_username.is_none()
|
||||
|| verge.webdav_password.is_none()
|
||||
{
|
||||
let msg = "Unable to create web dav client, please make sure the webdav config is correct".to_string();
|
||||
return Err(anyhow::Error::msg(msg));
|
||||
}
|
||||
let url = verge.webdav_url.unwrap_or_default();
|
||||
let username = verge.webdav_username.unwrap_or_default();
|
||||
let password = verge.webdav_password.unwrap_or_default();
|
||||
let url = url.trim_end_matches('/');
|
||||
let client = reqwest_dav::ClientBuilder::new()
|
||||
.set_agent(
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(3))
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.set_host(url.to_owned())
|
||||
.set_auth(reqwest_dav::Auth::Basic(
|
||||
username.to_owned(),
|
||||
password.to_owned(),
|
||||
))
|
||||
.build()?;
|
||||
|
||||
let config = WebDavConfig {
|
||||
url: verge
|
||||
.webdav_url
|
||||
.unwrap_or_default()
|
||||
.trim_end_matches('/')
|
||||
.to_string(),
|
||||
username: verge.webdav_username.unwrap_or_default(),
|
||||
password: verge.webdav_password.unwrap_or_default(),
|
||||
};
|
||||
|
||||
*lock = Some(config.clone());
|
||||
config
|
||||
if (client
|
||||
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
|
||||
.await)
|
||||
.is_err()
|
||||
{
|
||||
client.mkcol(dirs::BACKUP_DIR).await?;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新的客户端
|
||||
let client = reqwest_dav::ClientBuilder::new()
|
||||
.set_agent(
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(op.timeout()))
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.set_host(config.url)
|
||||
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
|
||||
.build()?;
|
||||
|
||||
// 确保备份目录存在
|
||||
let list_result = client
|
||||
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
|
||||
.await;
|
||||
if list_result.is_err() {
|
||||
client.mkcol(dirs::BACKUP_DIR).await?;
|
||||
*self.client.lock() = Some(client.clone());
|
||||
}
|
||||
|
||||
// 缓存客户端
|
||||
{
|
||||
let mut clients = self.clients.lock();
|
||||
clients.insert(op, client.clone());
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
Ok(self.client.lock().clone().unwrap())
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
*self.config.lock() = None;
|
||||
self.clients.lock().clear();
|
||||
if !self.client.lock().is_none() {
|
||||
self.client.lock().take();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
|
||||
let client = self.get_client(Operation::Upload).await?;
|
||||
let client = self.get_client().await?;
|
||||
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
|
||||
let fut = client.put(webdav_path.as_ref(), fs::read(file_path)?);
|
||||
timeout(Duration::from_secs(TIMEOUT_UPLOAD), fut).await??;
|
||||
client
|
||||
.put(webdav_path.as_ref(), fs::read(file_path)?)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
|
||||
let client = self.get_client(Operation::Download).await?;
|
||||
let client = self.get_client().await?;
|
||||
let path = format!("{}/{}", dirs::BACKUP_DIR, filename);
|
||||
|
||||
let fut = async {
|
||||
let response = client.get(path.as_str()).await?;
|
||||
let content = response.bytes().await?;
|
||||
fs::write(&storage_path, &content)?;
|
||||
Ok::<(), Error>(())
|
||||
};
|
||||
|
||||
timeout(Duration::from_secs(TIMEOUT_DOWNLOAD), fut).await??;
|
||||
let response = client.get(path.as_str()).await?;
|
||||
let content = response.bytes().await?;
|
||||
fs::write(&storage_path, &content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<Vec<ListFile>, Error> {
|
||||
let client = self.get_client(Operation::List).await?;
|
||||
let client = self.get_client().await?;
|
||||
let path = format!("{}/", dirs::BACKUP_DIR);
|
||||
|
||||
let fut = async {
|
||||
let files = client
|
||||
.list(path.as_str(), reqwest_dav::Depth::Number(1))
|
||||
.await?;
|
||||
let mut final_files = Vec::new();
|
||||
for file in files {
|
||||
if let ListEntity::File(file) = file {
|
||||
final_files.push(file);
|
||||
}
|
||||
let files = client
|
||||
.list(path.as_str(), reqwest_dav::Depth::Number(1))
|
||||
.await?;
|
||||
let mut final_files = Vec::new();
|
||||
for file in files {
|
||||
if let ListEntity::File(file) = file {
|
||||
final_files.push(file);
|
||||
}
|
||||
Ok::<Vec<ListFile>, Error>(final_files)
|
||||
};
|
||||
|
||||
timeout(Duration::from_secs(TIMEOUT_LIST), fut).await?
|
||||
}
|
||||
Ok(final_files)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, file_name: String) -> Result<(), Error> {
|
||||
let client = self.get_client(Operation::Delete).await?;
|
||||
let client = self.get_client().await?;
|
||||
let path = format!("{}/{}", dirs::BACKUP_DIR, file_name);
|
||||
|
||||
let fut = client.delete(&path);
|
||||
timeout(Duration::from_secs(TIMEOUT_DELETE), fut).await??;
|
||||
client.delete(&path).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
141
src-tauri/src/core/clash_api.rs
Normal file
141
src-tauri/src/core/clash_api.rs
Normal file
@ -0,0 +1,141 @@
|
||||
use crate::config::Config;
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// PUT /configs
|
||||
/// path 是绝对路径
|
||||
pub async fn put_configs(path: &str) -> Result<()> {
|
||||
let (url, headers) = clash_client_info()?;
|
||||
let url = format!("{url}/configs?force=true");
|
||||
|
||||
let mut data = HashMap::new();
|
||||
data.insert("path", path);
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client.put(&url).headers(headers).json(&data);
|
||||
let response = builder.send().await?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
204 => Ok(()),
|
||||
status => {
|
||||
bail!("failed to put configs with status \"{status}\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PATCH /configs
|
||||
pub async fn patch_configs(config: &Mapping) -> Result<()> {
|
||||
let (url, headers) = clash_client_info()?;
|
||||
let url = format!("{url}/configs");
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client.patch(&url).headers(headers.clone()).json(config);
|
||||
builder.send().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct DelayRes {
|
||||
delay: u64,
|
||||
}
|
||||
|
||||
/// GET /proxies/{name}/delay
|
||||
/// 获取代理延迟
|
||||
pub async fn get_proxy_delay(
|
||||
name: String,
|
||||
test_url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> Result<DelayRes> {
|
||||
let (url, headers) = clash_client_info()?;
|
||||
let url = format!("{url}/proxies/{name}/delay");
|
||||
|
||||
let default_url = "http://cp.cloudflare.com/generate_204";
|
||||
let test_url = test_url
|
||||
.map(|s| if s.is_empty() { default_url.into() } else { s })
|
||||
.unwrap_or(default_url.into());
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client
|
||||
.get(&url)
|
||||
.headers(headers)
|
||||
.query(&[("timeout", &format!("{timeout}")), ("url", &test_url)]);
|
||||
let response = builder.send().await?;
|
||||
|
||||
Ok(response.json::<DelayRes>().await?)
|
||||
}
|
||||
|
||||
/// 根据clash info获取clash服务地址和请求头
|
||||
fn clash_client_info() -> Result<(String, HeaderMap)> {
|
||||
let client = { Config::clash().data().get_client_info() };
|
||||
|
||||
let server = format!("http://{}", client.server);
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse()?);
|
||||
|
||||
if let Some(secret) = client.secret {
|
||||
let secret = format!("Bearer {}", secret).parse()?;
|
||||
headers.insert("Authorization", secret);
|
||||
}
|
||||
|
||||
Ok((server, headers))
|
||||
}
|
||||
|
||||
/// 缩短clash的日志
|
||||
#[allow(dead_code)]
|
||||
pub fn parse_log(log: String) -> String {
|
||||
if log.starts_with("time=") && log.len() > 33 {
|
||||
return (log[33..]).to_owned();
|
||||
}
|
||||
if log.len() > 9 {
|
||||
return (log[9..]).to_owned();
|
||||
}
|
||||
log
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn parse_check_output(log: String) -> String {
|
||||
let t = log.find("time=");
|
||||
let m = log.find("msg=");
|
||||
let mr = log.rfind('"');
|
||||
|
||||
if let (Some(_), Some(m), Some(mr)) = (t, m, mr) {
|
||||
let e = match log.find("level=error msg=") {
|
||||
Some(e) => e + 17,
|
||||
None => m + 5,
|
||||
};
|
||||
|
||||
if mr > m {
|
||||
return (log[e..mr]).to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
let l = log.find("error=");
|
||||
let r = log.find("path=").or(Some(log.len()));
|
||||
|
||||
if let (Some(l), Some(r)) = (l, r) {
|
||||
return (log[(l + 6)..(r - 1)]).to_owned();
|
||||
}
|
||||
|
||||
log
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_check_output() {
|
||||
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;
|
||||
//let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
|
||||
let str3 = r#"
|
||||
"time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress"
|
||||
time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'"
|
||||
configuration file xxx\n
|
||||
"#;
|
||||
|
||||
let res1 = parse_check_output(str1.into());
|
||||
// let res2 = parse_check_output(str2.into());
|
||||
let res3 = parse_check_output(str3.into());
|
||||
|
||||
assert_eq!(res1, res3);
|
||||
}
|
@ -1,676 +1,173 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::core::tray::Tray;
|
||||
use crate::{
|
||||
config::*,
|
||||
core::{
|
||||
handle,
|
||||
service::{self},
|
||||
},
|
||||
logging, logging_error,
|
||||
module::mihomo::MihomoManager,
|
||||
utils::{
|
||||
dirs,
|
||||
help::{self},
|
||||
logging::Type,
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use crate::config::*;
|
||||
use crate::core::{clash_api, handle, service};
|
||||
use crate::log_err;
|
||||
use crate::utils::dirs;
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::{fmt, path::PathBuf, sync::Arc};
|
||||
use tauri_plugin_shell::{process::CommandChild, ShellExt};
|
||||
use serde_yaml::Mapping;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CoreManager {
|
||||
running: Arc<Mutex<RunningMode>>,
|
||||
child_sidecar: Arc<Mutex<Option<CommandChild>>>,
|
||||
}
|
||||
|
||||
/// 内核运行模式
|
||||
#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
|
||||
pub enum RunningMode {
|
||||
/// 服务模式运行
|
||||
Service,
|
||||
/// Sidecar 模式运行
|
||||
Sidecar,
|
||||
/// 未运行
|
||||
NotRunning,
|
||||
}
|
||||
|
||||
impl fmt::Display for RunningMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RunningMode::Service => write!(f, "Service"),
|
||||
RunningMode::Sidecar => write!(f, "Sidecar"),
|
||||
RunningMode::NotRunning => write!(f, "NotRunning"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CLASH_CORES: [&str; 2] = ["verge-mihomo", "verge-mihomo-alpha"];
|
||||
|
||||
impl CoreManager {
|
||||
/// 检查文件是否为脚本文件
|
||||
fn is_script_file(&self, path: &str) -> Result<bool> {
|
||||
// 1. 先通过扩展名快速判断
|
||||
if path.ends_with(".yaml") || path.ends_with(".yml") {
|
||||
return Ok(false); // YAML文件不是脚本文件
|
||||
} else if path.ends_with(".js") {
|
||||
return Ok(true); // JS文件是脚本文件
|
||||
}
|
||||
|
||||
// 2. 读取文件内容
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"无法读取文件以检测类型: {}, 错误: {}",
|
||||
path,
|
||||
err
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to read file to detect type: {}",
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 检查是否存在明显的YAML特征
|
||||
let has_yaml_features = content.contains(": ")
|
||||
|| content.contains("#")
|
||||
|| content.contains("---")
|
||||
|| content.lines().any(|line| line.trim().starts_with("- "));
|
||||
|
||||
// 4. 检查是否存在明显的JS特征
|
||||
let has_js_features = content.contains("function ")
|
||||
|| content.contains("const ")
|
||||
|| content.contains("let ")
|
||||
|| content.contains("var ")
|
||||
|| content.contains("//")
|
||||
|| content.contains("/*")
|
||||
|| content.contains("*/")
|
||||
|| content.contains("export ")
|
||||
|| content.contains("import ");
|
||||
|
||||
// 5. 决策逻辑
|
||||
if has_yaml_features && !has_js_features {
|
||||
// 只有YAML特征,没有JS特征
|
||||
return Ok(false);
|
||||
} else if has_js_features && !has_yaml_features {
|
||||
// 只有JS特征,没有YAML特征
|
||||
return Ok(true);
|
||||
} else if has_yaml_features && has_js_features {
|
||||
// 两种特征都有,需要更精细判断
|
||||
// 优先检查是否有明确的JS结构特征
|
||||
if content.contains("function main")
|
||||
|| content.contains("module.exports")
|
||||
|| content.contains("export default")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// 检查冒号后是否有空格(YAML的典型特征)
|
||||
let yaml_pattern_count = content.lines().filter(|line| line.contains(": ")).count();
|
||||
|
||||
if yaml_pattern_count > 2 {
|
||||
return Ok(false); // 多个键值对格式,更可能是YAML
|
||||
}
|
||||
}
|
||||
|
||||
// 默认情况:无法确定时,假设为非脚本文件(更安全)
|
||||
logging!(
|
||||
debug,
|
||||
Type::Config,
|
||||
true,
|
||||
"无法确定文件类型,默认当作YAML处理: {}",
|
||||
path
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
/// 使用默认配置
|
||||
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
config: Some(Config::clash().latest().0.clone()),
|
||||
exists_keys: vec![],
|
||||
chain_logs: Default::default(),
|
||||
};
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
)?;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
Ok(())
|
||||
}
|
||||
/// 验证运行时配置
|
||||
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||
logging!(info, Type::Config, true, "生成临时配置文件用于验证");
|
||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
self.validate_config_internal(config_path).await
|
||||
}
|
||||
/// 验证指定的配置文件
|
||||
pub async fn validate_config_file(
|
||||
&self,
|
||||
config_path: &str,
|
||||
is_merge_file: Option<bool>,
|
||||
) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if !std::path::Path::new(config_path).exists() {
|
||||
let error_msg = format!("File not found: {}", config_path);
|
||||
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
|
||||
// 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证
|
||||
if is_merge_file.unwrap_or(false) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"检测到Merge文件,仅进行语法检查: {}",
|
||||
config_path
|
||||
);
|
||||
return self.validate_file_syntax(config_path).await;
|
||||
}
|
||||
|
||||
// 检查是否为脚本文件
|
||||
let is_script = if config_path.ends_with(".js") {
|
||||
true
|
||||
} else {
|
||||
match self.is_script_file(config_path) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
// 如果无法确定文件类型,尝试使用Clash内核验证
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"无法确定文件类型: {}, 错误: {}",
|
||||
config_path,
|
||||
err
|
||||
);
|
||||
return self.validate_config_internal(config_path).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if is_script {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"检测到脚本文件,使用JavaScript验证: {}",
|
||||
config_path
|
||||
);
|
||||
return self.validate_script_file(config_path).await;
|
||||
}
|
||||
|
||||
// 对YAML配置文件使用Clash内核验证
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"使用Clash内核验证配置文件: {}",
|
||||
config_path
|
||||
);
|
||||
self.validate_config_internal(config_path).await
|
||||
}
|
||||
/// 内部验证配置文件的实现
|
||||
async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"开始验证配置文件: {}",
|
||||
config_path
|
||||
);
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let app_dir_str = dirs::path_to_str(&app_dir)?;
|
||||
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
|
||||
|
||||
// 使用子进程运行clash验证配置
|
||||
let output = app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core)?
|
||||
.args(["-t", "-d", app_dir_str, "-f", config_path])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// 检查进程退出状态和错误输出
|
||||
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
|
||||
let has_error =
|
||||
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||
|
||||
logging!(info, Type::Config, true, "-------- 验证结果 --------");
|
||||
|
||||
if !stderr.is_empty() {
|
||||
logging!(info, Type::Config, true, "stderr输出:\n{}", stderr);
|
||||
}
|
||||
|
||||
if has_error {
|
||||
logging!(info, Type::Config, true, "发现错误,开始处理错误信息");
|
||||
let error_msg = if !stdout.is_empty() {
|
||||
stdout.to_string()
|
||||
} else if !stderr.is_empty() {
|
||||
stderr.to_string()
|
||||
} else if let Some(code) = output.status.code() {
|
||||
format!("验证进程异常退出,退出码: {}", code)
|
||||
} else {
|
||||
"验证进程被终止".to_string()
|
||||
};
|
||||
|
||||
logging!(info, Type::Config, true, "-------- 验证结束 --------");
|
||||
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "验证成功");
|
||||
logging!(info, Type::Config, true, "-------- 验证结束 --------");
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
}
|
||||
/// 只进行文件语法检查,不进行完整验证
|
||||
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
|
||||
|
||||
// 读取文件内容
|
||||
let content = match std::fs::read_to_string(config_path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read file: {}", err);
|
||||
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
};
|
||||
// 对YAML文件尝试解析,只检查语法正确性
|
||||
logging!(info, Type::Config, true, "进行YAML语法检查");
|
||||
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Config, true, "YAML语法检查通过");
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
Err(err) => {
|
||||
// 使用标准化的前缀,以便错误处理函数能正确识别
|
||||
let error_msg = format!("YAML syntax error: {}", err);
|
||||
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 验证脚本文件语法
|
||||
async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
|
||||
// 读取脚本内容
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read script file: {}", err);
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
};
|
||||
|
||||
logging!(debug, Type::Config, true, "验证脚本文件: {}", path);
|
||||
|
||||
// 使用boa引擎进行基本语法检查
|
||||
use boa_engine::{Context, Source};
|
||||
|
||||
let mut context = Context::default();
|
||||
let result = context.eval(Source::from_bytes(&content));
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
logging!(debug, Type::Config, true, "脚本语法验证通过: {}", path);
|
||||
|
||||
// 检查脚本是否包含main函数
|
||||
if !content.contains("function main")
|
||||
&& !content.contains("const main")
|
||||
&& !content.contains("let main")
|
||||
{
|
||||
let error_msg = "Script must contain a main function";
|
||||
logging!(warn, Type::Config, true, "脚本缺少main函数: {}", path);
|
||||
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
|
||||
return Ok((false, error_msg.to_string()));
|
||||
}
|
||||
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Script syntax error: {}", err);
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 更新proxies等配置
|
||||
pub async fn update_config(&self) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过完整验证流程
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Config, true, "应用正在退出,跳过验证");
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
logging!(info, Type::Config, true, "开始更新配置");
|
||||
|
||||
// 1. 先生成新的配置内容
|
||||
logging!(info, Type::Config, true, "生成新的配置内容");
|
||||
Config::generate().await?;
|
||||
|
||||
// 2. 验证配置
|
||||
match self.validate_config().await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, true, "配置验证通过");
|
||||
// 4. 验证通过后,生成正式的运行时配置
|
||||
logging!(info, Type::Config, true, "生成运行时配置");
|
||||
let run_path = Config::generate_file(ConfigType::Run)?;
|
||||
logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
|
||||
Ok((true, "something".into()))
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
|
||||
Config::runtime().discard();
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
|
||||
Config::runtime().discard();
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn put_configs_force(&self, path_buf: PathBuf) -> Result<(), String> {
|
||||
let run_path_str = dirs::path_to_str(&path_buf).map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
logging_error!(Type::Core, true, "{}", msg);
|
||||
msg
|
||||
});
|
||||
match MihomoManager::global()
|
||||
.put_configs_force(run_path_str?)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
Config::runtime().apply();
|
||||
logging!(info, Type::Core, true, "Configuration updated successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
Config::runtime().discard();
|
||||
logging_error!(Type::Core, true, "Failed to update configuration: {}", msg);
|
||||
Err(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
async fn start_core_by_sidecar(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, true, "Running core by sidecar");
|
||||
let config_file = &Config::generate_file(ConfigType::Run)?;
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
|
||||
let clash_core = Config::verge()
|
||||
.latest()
|
||||
.clash_core
|
||||
.clone()
|
||||
.unwrap_or("verge-mihomo".to_string());
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
let (_, child) = app_handle
|
||||
.shell()
|
||||
.sidecar(&clash_core)?
|
||||
.args([
|
||||
"-d",
|
||||
dirs::path_to_str(&config_dir)?,
|
||||
"-f",
|
||||
dirs::path_to_str(config_file)?,
|
||||
])
|
||||
.spawn()?;
|
||||
let pid = child.pid();
|
||||
logging!(
|
||||
trace,
|
||||
Type::Core,
|
||||
true,
|
||||
"Started core by sidecar pid: {}",
|
||||
pid
|
||||
);
|
||||
*self.child_sidecar.lock().await = Some(child);
|
||||
self.set_running_mode(RunningMode::Sidecar).await;
|
||||
Ok(())
|
||||
}
|
||||
async fn stop_core_by_sidecar(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, true, "Stopping core by sidecar");
|
||||
|
||||
if let Some(child) = self.child_sidecar.lock().await.take() {
|
||||
let pid = child.pid();
|
||||
child.kill()?;
|
||||
logging!(
|
||||
trace,
|
||||
Type::Core,
|
||||
true,
|
||||
"Stopped core by sidecar pid: {}",
|
||||
pid
|
||||
);
|
||||
}
|
||||
self.set_running_mode(RunningMode::NotRunning).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
async fn start_core_by_service(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, true, "Running core by service");
|
||||
let config_file = &Config::generate_file(ConfigType::Run)?;
|
||||
service::run_core_by_service(config_file).await?;
|
||||
self.set_running_mode(RunningMode::Service).await;
|
||||
Ok(())
|
||||
}
|
||||
async fn stop_core_by_service(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, true, "Stopping core by service");
|
||||
service::stop_core_by_service().await?;
|
||||
self.set_running_mode(RunningMode::NotRunning).await;
|
||||
Ok(())
|
||||
}
|
||||
running: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
pub fn global() -> &'static CoreManager {
|
||||
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
||||
CORE_MANAGER.get_or_init(|| CoreManager {
|
||||
running: Arc::new(Mutex::new(RunningMode::NotRunning)),
|
||||
child_sidecar: Arc::new(Mutex::new(None)),
|
||||
running: Arc::new(Mutex::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, "Initializing core");
|
||||
|
||||
if service::is_service_available().await.is_ok() {
|
||||
logging!(info, Type::Core, true, "服务可用,直接使用服务模式");
|
||||
|
||||
// 检查版本是否需要重装
|
||||
if service::check_service_needs_reinstall().await {
|
||||
logging!(info, Type::Core, true, "服务版本不匹配,执行重装");
|
||||
service::reinstall_service().await?;
|
||||
}
|
||||
|
||||
self.start_core_by_service().await?;
|
||||
} else {
|
||||
// 服务不可用,获取服务状态
|
||||
let service_state = service::ServiceState::get();
|
||||
let has_service_install_record = service_state.last_install_time > 0;
|
||||
|
||||
if service_state.prefer_sidecar {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"用户偏好Sidecar模式,使用Sidecar模式启动"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
// 检查是否已经有服务安装记录,如果没有,则尝试安装
|
||||
else if !has_service_install_record {
|
||||
logging!(info, Type::Core, true, "首次运行,服务不可用,尝试安装");
|
||||
|
||||
match service::install_service().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Core, true, "服务安装成功");
|
||||
|
||||
let mut new_state = service::ServiceState::default();
|
||||
new_state.record_install();
|
||||
new_state.prefer_sidecar = false;
|
||||
new_state.save()?;
|
||||
|
||||
if service::is_service_available().await.is_ok() {
|
||||
self.start_core_by_service().await?;
|
||||
logging!(info, Type::Core, true, "服务启动成功");
|
||||
} else {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务安装成功但未能连接,回退到Sidecar模式"
|
||||
);
|
||||
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// 安装失败,记录错误并使用sidecar模式
|
||||
logging!(warn, Type::Core, true, "服务安装失败: {}", err);
|
||||
|
||||
let new_state = service::ServiceState {
|
||||
last_error: Some(err.to_string()),
|
||||
prefer_sidecar: true,
|
||||
..Default::default()
|
||||
};
|
||||
new_state.save()?;
|
||||
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"有服务安装记录但服务不可用,使用Sidecar模式"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
|
||||
logging!(trace, Type::Core, "Initied core");
|
||||
#[cfg(target_os = "macos")]
|
||||
logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
|
||||
log::trace!("run core start");
|
||||
// 启动clash
|
||||
log_err!(Self::global().start_core().await);
|
||||
log::trace!("run core end");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_running_mode(&self, mode: RunningMode) {
|
||||
let mut guard = self.running.lock().await;
|
||||
*guard = mode;
|
||||
}
|
||||
/// 检查订阅是否正确
|
||||
pub async fn check_config(&self) -> Result<()> {
|
||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
|
||||
pub async fn get_running_mode(&self) -> RunningMode {
|
||||
let guard = self.running.lock().await;
|
||||
(*guard).clone()
|
||||
}
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
|
||||
let test_dir = dirs::app_home_dir()?.join("test");
|
||||
let test_dir = dirs::path_to_str(&test_dir)?;
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
|
||||
let _ = app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core)?
|
||||
.args(["-t", "-d", test_dir, "-f", config_path])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
/// 启动核心
|
||||
pub async fn start_core(&self) -> Result<()> {
|
||||
if service::is_service_available().await.is_ok() {
|
||||
if service::check_service_needs_reinstall().await {
|
||||
service::reinstall_service().await?;
|
||||
}
|
||||
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
|
||||
self.start_core_by_service().await?;
|
||||
} else {
|
||||
// 服务不可用,检查用户偏好
|
||||
let service_state = service::ServiceState::get();
|
||||
if service_state.prefer_sidecar {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务不可用,根据用户偏好使用Sidecar模式"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
} else {
|
||||
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止核心运行
|
||||
pub async fn stop_core(&self) -> Result<()> {
|
||||
match self.get_running_mode().await {
|
||||
RunningMode::Service => self.stop_core_by_service().await,
|
||||
RunningMode::Sidecar => self.stop_core_by_sidecar().await,
|
||||
RunningMode::NotRunning => Ok(()),
|
||||
let mut running = self.running.lock().await;
|
||||
|
||||
if !*running {
|
||||
log::debug!("core is not running");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 关闭tun模式
|
||||
let mut disable = Mapping::new();
|
||||
let mut tun = Mapping::new();
|
||||
tun.insert("enable".into(), false.into());
|
||||
disable.insert("tun".into(), tun.into());
|
||||
log::debug!(target: "app", "disable tun mode");
|
||||
log_err!(clash_api::patch_configs(&disable).await);
|
||||
|
||||
// 服务模式
|
||||
if service::check_service().await.is_ok() {
|
||||
log::info!(target: "app", "stop the core by service");
|
||||
service::stop_core_by_service().await?;
|
||||
}
|
||||
*running = false;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 启动核心
|
||||
pub async fn start_core(&self) -> Result<()> {
|
||||
let mut running = self.running.lock().await;
|
||||
if *running {
|
||||
log::info!("core is running");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config_path = Config::generate_file(ConfigType::Run)?;
|
||||
|
||||
// 服务模式
|
||||
if service::check_service().await.is_ok() {
|
||||
log::info!(target: "app", "try to run core in service mode");
|
||||
service::run_core_by_service(&config_path).await?;
|
||||
*running = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重启内核
|
||||
pub async fn restart_core(&self) -> Result<()> {
|
||||
// 重新启动app
|
||||
self.stop_core().await?;
|
||||
self.start_core().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 切换核心
|
||||
pub async fn change_core(&self, clash_core: Option<String>) -> Result<(), String> {
|
||||
if clash_core.is_none() {
|
||||
let error_message = "Clash core should not be Null";
|
||||
logging!(error, Type::Core, true, "{}", error_message);
|
||||
return Err(error_message.to_string());
|
||||
}
|
||||
let core: &str = &clash_core.clone().unwrap();
|
||||
if !CLASH_CORES.contains(&core) {
|
||||
let error_message = format!("Clash core invalid name: {}", core);
|
||||
logging!(error, Type::Core, true, "{}", error_message);
|
||||
return Err(error_message);
|
||||
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
||||
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
||||
const CLASH_CORES: [&str; 2] = ["verge-mihomo", "verge-mihomo-alpha"];
|
||||
|
||||
if !CLASH_CORES.contains(&clash_core.as_str()) {
|
||||
bail!("invalid clash core name \"{clash_core}\"");
|
||||
}
|
||||
|
||||
Config::verge().draft().clash_core = clash_core.clone();
|
||||
Config::verge().apply();
|
||||
logging_error!(Type::Core, true, Config::verge().latest().save_file());
|
||||
log::info!(target: "app", "change core to `{clash_core}`");
|
||||
|
||||
let run_path = Config::generate_file(ConfigType::Run).map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
logging_error!(Type::Core, true, "{}", msg);
|
||||
msg
|
||||
})?;
|
||||
Config::verge().draft().clash_core = Some(clash_core);
|
||||
|
||||
self.put_configs_force(run_path).await?;
|
||||
// 更新订阅
|
||||
Config::generate().await?;
|
||||
|
||||
self.check_config().await?;
|
||||
|
||||
match self.restart_core().await {
|
||||
Ok(_) => {
|
||||
Config::verge().apply();
|
||||
Config::runtime().apply();
|
||||
log_err!(Config::verge().latest().save_file());
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::verge().discard();
|
||||
Config::runtime().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新proxies那些
|
||||
/// 如果涉及端口和外部控制则需要重启
|
||||
pub async fn update_config(&self) -> Result<()> {
|
||||
log::debug!(target: "app", "try to update clash config");
|
||||
// 更新订阅
|
||||
Config::generate().await?;
|
||||
|
||||
// 检查订阅是否正常
|
||||
self.check_config().await?;
|
||||
|
||||
// 更新运行时订阅
|
||||
let path = Config::generate_file(ConfigType::Run)?;
|
||||
let path = dirs::path_to_str(&path)?;
|
||||
|
||||
// 发送请求 发送5次
|
||||
for i in 0..10 {
|
||||
match clash_api::put_configs(path).await {
|
||||
Ok(_) => break,
|
||||
Err(err) => {
|
||||
if i < 9 {
|
||||
log::info!(target: "app", "{err}");
|
||||
} else {
|
||||
bail!(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,15 @@
|
||||
use super::tray::Tray;
|
||||
use crate::log_err;
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||
|
||||
use crate::{logging, logging_error, utils::logging::Type};
|
||||
|
||||
/// 存储启动期间的错误消息
|
||||
#[derive(Debug, Clone)]
|
||||
struct ErrorMessage {
|
||||
status: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Handle {
|
||||
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||
pub is_exiting: Arc<RwLock<bool>>,
|
||||
/// 存储启动过程中产生的错误消息队列
|
||||
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||
startup_completed: Arc<RwLock<bool>>,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
@ -28,8 +19,6 @@ impl Handle {
|
||||
HANDLE.get_or_init(|| Handle {
|
||||
app_handle: Arc::new(RwLock::new(None)),
|
||||
is_exiting: Arc::new(RwLock::new(false)),
|
||||
startup_errors: Arc::new(RwLock::new(Vec::new())),
|
||||
startup_completed: Arc::new(RwLock::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
@ -53,116 +42,33 @@ impl Handle {
|
||||
|
||||
pub fn refresh_clash() {
|
||||
if let Some(window) = Self::global().get_window() {
|
||||
logging_error!(
|
||||
Type::Frontend,
|
||||
true,
|
||||
window.emit("verge://refresh-clash-config", "yes")
|
||||
);
|
||||
log_err!(window.emit("verge://refresh-clash-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_verge() {
|
||||
if let Some(window) = Self::global().get_window() {
|
||||
logging_error!(
|
||||
Type::Frontend,
|
||||
true,
|
||||
window.emit("verge://refresh-verge-config", "yes")
|
||||
);
|
||||
log_err!(window.emit("verge://refresh-verge-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn refresh_profiles() {
|
||||
if let Some(window) = Self::global().get_window() {
|
||||
logging_error!(
|
||||
Type::Frontend,
|
||||
true,
|
||||
window.emit("verge://refresh-profiles-config", "yes")
|
||||
);
|
||||
log_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知前端显示消息,如果在启动过程中,则将消息存入启动错误队列
|
||||
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||
let handle = Self::global();
|
||||
let status_str = status.into();
|
||||
let msg_str = msg.into();
|
||||
|
||||
// 检查是否正在启动过程中
|
||||
if !*handle.startup_completed.read() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Frontend,
|
||||
true,
|
||||
"启动过程中发现错误,加入消息队列: {} - {}",
|
||||
status_str,
|
||||
msg_str
|
||||
);
|
||||
|
||||
// 将消息添加到启动错误队列
|
||||
let mut errors = handle.startup_errors.write();
|
||||
errors.push(ErrorMessage {
|
||||
status: status_str,
|
||||
message: msg_str,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 已经完成启动,直接发送消息
|
||||
if let Some(window) = handle.get_window() {
|
||||
logging_error!(
|
||||
Type::Frontend,
|
||||
true,
|
||||
window.emit("verge://notice-message", (status_str, msg_str))
|
||||
);
|
||||
if let Some(window) = Self::global().get_window() {
|
||||
log_err!(window.emit("verge://notice-message", (status.into(), msg.into())));
|
||||
}
|
||||
}
|
||||
|
||||
/// 标记启动已完成,并发送所有启动阶段累积的错误消息
|
||||
pub fn mark_startup_completed(&self) {
|
||||
{
|
||||
let mut completed = self.startup_completed.write();
|
||||
*completed = true;
|
||||
}
|
||||
|
||||
self.send_startup_errors();
|
||||
}
|
||||
|
||||
/// 发送启动时累积的所有错误消息
|
||||
fn send_startup_errors(&self) {
|
||||
let errors = {
|
||||
let mut errors = self.startup_errors.write();
|
||||
std::mem::take(&mut *errors)
|
||||
};
|
||||
|
||||
if errors.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Frontend,
|
||||
true,
|
||||
"发送{}条启动时累积的错误消息",
|
||||
errors.len()
|
||||
);
|
||||
|
||||
// 等待2秒以确保前端已完全加载,延迟发送错误通知
|
||||
if let Some(window) = self.get_window() {
|
||||
let window_clone = window.clone();
|
||||
let errors_clone = errors.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
for error in errors_clone {
|
||||
let _ =
|
||||
window_clone.emit("verge://notice-message", (error.status, error.message));
|
||||
// 每条消息之间间隔500ms,避免消息堆积
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
/// update the system tray state
|
||||
pub fn update_systray_part() -> Result<()> {
|
||||
Tray::update_part()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_is_exiting(&self) {
|
||||
|
@ -1,17 +1,11 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::handle,
|
||||
feat, logging, logging_error,
|
||||
module::lightweight::entry_lightweight_mode,
|
||||
utils::{logging::Type, resolve},
|
||||
};
|
||||
use crate::core::handle;
|
||||
use crate::{config::Config, feat, log_err};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::{async_runtime, Manager};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
}
|
||||
@ -27,30 +21,8 @@ impl Hotkey {
|
||||
|
||||
pub fn init(&self) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Initializing hotkeys with enable: {}",
|
||||
enable_global_hotkey
|
||||
);
|
||||
|
||||
// 如果全局热键被禁用,则不注册热键
|
||||
if !enable_global_hotkey {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Has {} hotkeys need to register",
|
||||
hotkeys.len()
|
||||
);
|
||||
|
||||
for hotkey in hotkeys.iter() {
|
||||
let mut iter = hotkey.split(',');
|
||||
let func = iter.next();
|
||||
@ -58,210 +30,56 @@ impl Hotkey {
|
||||
|
||||
match (key, func) {
|
||||
(Some(key), Some(func)) => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Registering hotkey: {} -> {}",
|
||||
key,
|
||||
func
|
||||
);
|
||||
if let Err(e) = self.register(key, func) {
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Failed to register hotkey {} -> {}: {:?}",
|
||||
key,
|
||||
func,
|
||||
e
|
||||
);
|
||||
} else {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Successfully registered hotkey {} -> {}",
|
||||
key,
|
||||
func
|
||||
);
|
||||
}
|
||||
log_err!(self.register(key, func));
|
||||
}
|
||||
_ => {
|
||||
let key = key.unwrap_or("None");
|
||||
let func = func.unwrap_or("None");
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Invalid hotkey configuration: `{}`:`{}`",
|
||||
key,
|
||||
func
|
||||
);
|
||||
log::error!(target: "app", "invalid hotkey `{key}`:`{func}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
self.current.lock().clone_from(hotkeys);
|
||||
} else {
|
||||
logging!(debug, Type::Hotkey, true, "No hotkeys configured");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let manager = app_handle.global_shortcut();
|
||||
manager.unregister_all()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let manager = app_handle.global_shortcut();
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Attempting to register hotkey: {} for function: {}",
|
||||
hotkey,
|
||||
func
|
||||
);
|
||||
|
||||
if manager.is_registered(hotkey) {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Hotkey {} was already registered, unregistering first",
|
||||
hotkey
|
||||
);
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
"open_or_close_dashboard" => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Registering open_or_close_dashboard function"
|
||||
);
|
||||
|| {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"=== Hotkey Dashboard Window Operation Start ==="
|
||||
);
|
||||
|
||||
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||
async_runtime::spawn_blocking(|| {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Toggle dashboard window visibility"
|
||||
);
|
||||
|
||||
// 检查窗口是否存在
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
// 如果窗口可见,则隐藏它
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
logging!(info, Type::Window, true, "Window is visible, hiding it");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
// 如果窗口不可见,则显示它
|
||||
logging!(info, Type::Window, true, "Window is hidden, showing it");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
} else {
|
||||
// 如果窗口不存在,创建一个新窗口
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window does not exist, creating a new one"
|
||||
);
|
||||
resolve::create_window(true);
|
||||
}
|
||||
});
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"=== Hotkey Dashboard Window Operation End ==="
|
||||
);
|
||||
}
|
||||
}
|
||||
"open_or_close_dashboard" => feat::open_or_close_dashboard,
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
||||
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
||||
"toggle_system_proxy" => feat::toggle_system_proxy,
|
||||
"toggle_tun_mode" => feat::toggle_tun_mode,
|
||||
"quit" => || feat::quit(Some(0)),
|
||||
#[cfg(target_os = "macos")]
|
||||
"hide" => || feat::hide(),
|
||||
|
||||
_ => {
|
||||
logging!(error, Type::Hotkey, true, "Invalid function: {}", func);
|
||||
bail!("invalid function \"{func}\"");
|
||||
}
|
||||
_ => bail!("invalid function \"{func}\""),
|
||||
};
|
||||
|
||||
let is_quit = func.trim() == "quit";
|
||||
|
||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
||||
if event.state == ShortcutState::Pressed {
|
||||
logging!(debug, Type::Hotkey, true, "Hotkey pressed: {:?}", hotkey);
|
||||
|
||||
if hotkey.key == Code::KeyQ && is_quit {
|
||||
if hotkey.key == Code::KeyQ {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if window.is_focused().unwrap_or(false) {
|
||||
logging!(debug, Type::Hotkey, true, "Executing quit function");
|
||||
f();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接执行函数,不做任何状态检查
|
||||
logging!(debug, Type::Hotkey, true, "Executing function directly");
|
||||
|
||||
// 获取全局热键状态
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
|
||||
if is_enable_global_hotkey {
|
||||
f();
|
||||
} else if let Some(window) = app_handle.get_webview_window("main") {
|
||||
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||
let is_visible = window.is_visible().unwrap_or(false);
|
||||
let is_focused = window.is_focused().unwrap_or(false);
|
||||
|
||||
if is_focused && is_visible {
|
||||
f();
|
||||
}
|
||||
}
|
||||
f();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Successfully registered hotkey {} for {}",
|
||||
hotkey,
|
||||
func
|
||||
);
|
||||
log::debug!(target: "app", "register hotkey {hotkey} {func}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -269,7 +87,8 @@ impl Hotkey {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let manager = app_handle.global_shortcut();
|
||||
manager.unregister(hotkey)?;
|
||||
logging!(debug, Type::Hotkey, true, "Unregister hotkey {}", hotkey);
|
||||
|
||||
log::debug!(target: "app", "unregister hotkey {hotkey}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -285,7 +104,7 @@ impl Hotkey {
|
||||
});
|
||||
|
||||
add.iter().for_each(|(key, func)| {
|
||||
logging_error!(Type::Hotkey, true, self.register(key, func));
|
||||
log_err!(self.register(key, func));
|
||||
});
|
||||
|
||||
*current = new_hotkeys;
|
||||
@ -342,13 +161,7 @@ impl Drop for Hotkey {
|
||||
fn drop(&mut self) {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Error unregistering all hotkeys: {:?}",
|
||||
e
|
||||
);
|
||||
log::error!(target:"app", "Error unregistering all hotkeys: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
pub mod backup;
|
||||
pub mod clash_api;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod core;
|
||||
pub mod handle;
|
||||
|
@ -1,95 +1,15 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
logging,
|
||||
utils::{dirs, logging::Type},
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::utils::dirs;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::current_exe,
|
||||
path::PathBuf,
|
||||
process::Command as StdCommand,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::{env::current_exe, process::Command as StdCommand};
|
||||
use tokio::time::Duration;
|
||||
|
||||
// Windows only
|
||||
|
||||
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
||||
const REQUIRED_SERVICE_VERSION: &str = "1.0.5"; // 定义所需的服务版本号
|
||||
|
||||
// 限制重装时间和次数的常量
|
||||
const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期
|
||||
const MAX_REINSTALLS_PER_DAY: u32 = 3; // 每24小时最多重装3次
|
||||
const ONE_DAY_SECS: u64 = 86400; // 24小时的秒数
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
pub struct ServiceState {
|
||||
pub last_install_time: u64, // 上次安装时间戳 (Unix 时间戳,秒)
|
||||
pub install_count: u32, // 24小时内安装次数
|
||||
pub last_check_time: u64, // 上次检查时间
|
||||
pub last_error: Option<String>, // 上次错误信息
|
||||
pub prefer_sidecar: bool, // 用户是否偏好sidecar模式,如拒绝安装服务或安装失败
|
||||
}
|
||||
|
||||
impl ServiceState {
|
||||
// 获取当前的服务状态
|
||||
pub fn get() -> Self {
|
||||
if let Some(state) = Config::verge().latest().service_state.clone() {
|
||||
return state;
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// 保存服务状态
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let config = Config::verge();
|
||||
let mut latest = config.latest().clone();
|
||||
latest.service_state = Some(self.clone());
|
||||
*config.draft() = latest;
|
||||
config.apply();
|
||||
Config::verge().latest().save_file()
|
||||
}
|
||||
|
||||
// 更新安装信息
|
||||
pub fn record_install(&mut self) {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
// 检查是否需要重置计数器(24小时已过)
|
||||
if now - self.last_install_time > ONE_DAY_SECS {
|
||||
self.install_count = 0;
|
||||
}
|
||||
|
||||
self.last_install_time = now;
|
||||
self.install_count += 1;
|
||||
}
|
||||
|
||||
// 检查是否可以重新安装
|
||||
pub fn can_reinstall(&self) -> bool {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
// 如果在冷却期内,不允许重装
|
||||
if now - self.last_install_time < REINSTALL_COOLDOWN_SECS {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果24小时内安装次数过多,也不允许
|
||||
if now - self.last_install_time < ONE_DAY_SECS
|
||||
&& self.install_count >= MAX_REINSTALLS_PER_DAY
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ResponseBody {
|
||||
@ -99,12 +19,6 @@ pub struct ResponseBody {
|
||||
pub log_file: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct VersionResponse {
|
||||
pub service: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct JsonResponse {
|
||||
pub code: u64,
|
||||
@ -112,50 +26,9 @@ pub struct JsonResponse {
|
||||
pub data: Option<ResponseBody>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct VersionJsonResponse {
|
||||
pub code: u64,
|
||||
pub msg: String,
|
||||
pub data: Option<VersionResponse>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "uninstall service");
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
let status = match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
||||
_ => StdCommand::new(uninstall_path)
|
||||
.creation_flags(0x08000000)
|
||||
.status()?,
|
||||
};
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn install_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "install service");
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
log::info!(target:"app", "reinstall service");
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
@ -163,13 +36,25 @@ pub async fn install_service() -> Result<()> {
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
let install_path = binary_path.with_file_name("install-service.exe");
|
||||
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
|
||||
|
||||
if !install_path.exists() {
|
||||
bail!(format!("installer not found: {install_path:?}"));
|
||||
}
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
let _ = match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
||||
_ => StdCommand::new(uninstall_path)
|
||||
.creation_flags(0x08000000)
|
||||
.status()?,
|
||||
};
|
||||
|
||||
let status = match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
|
||||
_ => StdCommand::new(install_path)
|
||||
@ -187,64 +72,24 @@ pub async fn install_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
let mut service_state = ServiceState::get();
|
||||
|
||||
// 检查是否允许重装
|
||||
if !service_state.can_reinstall() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"service reinstall rejected: cooldown period or max attempts reached"
|
||||
);
|
||||
bail!("Service reinstallation is rate limited. Please try again later.");
|
||||
}
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"failed to uninstall service: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service().await {
|
||||
Ok(_) => {
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
service_state.last_error = None;
|
||||
service_state.save()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.save()?;
|
||||
bail!(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "uninstall service");
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
log::info!(target:"app", "reinstall service");
|
||||
use users::get_effective_uid;
|
||||
|
||||
let install_path = tauri::utils::platform::current_exe()?.with_file_name("install-service");
|
||||
|
||||
let uninstall_path = tauri::utils::platform::current_exe()?.with_file_name("uninstall-service");
|
||||
|
||||
if !install_path.exists() {
|
||||
bail!(format!("installer not found: {install_path:?}"));
|
||||
}
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let install_shell: String = install_path.to_string_lossy().replace(" ", "\\ ");
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().replace(" ", "\\ ");
|
||||
|
||||
let elevator = crate::utils::help::linux_elevator();
|
||||
@ -256,38 +101,8 @@ pub async fn uninstall_service() -> Result<()> {
|
||||
.arg(uninstall_shell)
|
||||
.status()?,
|
||||
};
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"uninstall status code:{}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
log::info!(target:"app", "status code:{}", status.code().unwrap());
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn install_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "install service");
|
||||
use users::get_effective_uid;
|
||||
|
||||
let install_path = tauri::utils::platform::current_exe()?.with_file_name("install-service");
|
||||
|
||||
if !install_path.exists() {
|
||||
bail!(format!("installer not found: {install_path:?}"));
|
||||
}
|
||||
|
||||
let install_shell: String = install_path.to_string_lossy().replace(" ", "\\ ");
|
||||
|
||||
let elevator = crate::utils::help::linux_elevator();
|
||||
let status = match get_effective_uid() {
|
||||
0 => StdCommand::new(install_shell).status()?,
|
||||
_ => StdCommand::new(elevator.clone())
|
||||
@ -296,13 +111,6 @@ pub async fn install_service() -> Result<()> {
|
||||
.arg(install_shell)
|
||||
.status()?,
|
||||
};
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"install status code:{}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
@ -314,110 +122,29 @@ pub async fn install_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
let mut service_state = ServiceState::get();
|
||||
|
||||
// 检查是否允许重装
|
||||
if !service_state.can_reinstall() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"service reinstall rejected: cooldown period or max attempts reached"
|
||||
);
|
||||
bail!("Service reinstallation is rate limited. Please try again later.");
|
||||
}
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"failed to uninstall service: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service().await {
|
||||
Ok(_) => {
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
service_state.last_error = None;
|
||||
service_state.save()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.save()?;
|
||||
bail!(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
use crate::utils::i18n::t;
|
||||
|
||||
logging!(info, Type::Service, true, "uninstall service");
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
let uninstall_path = binary_path.with_file_name("uninstall-service");
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||
|
||||
let prompt = t("Service Administrator Prompt");
|
||||
let command = format!(
|
||||
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||
);
|
||||
|
||||
logging!(debug, Type::Service, true, "uninstall command: {}", command);
|
||||
|
||||
let status = StdCommand::new("osascript")
|
||||
.args(vec!["-e", &command])
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn install_service() -> Result<()> {
|
||||
use crate::utils::i18n::t;
|
||||
|
||||
logging!(info, Type::Service, true, "install service");
|
||||
log::info!(target:"app", "reinstall service");
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
let install_path = binary_path.with_file_name("install-service");
|
||||
let uninstall_path = binary_path.with_file_name("uninstall-service");
|
||||
|
||||
if !install_path.exists() {
|
||||
bail!(format!("installer not found: {install_path:?}"));
|
||||
}
|
||||
|
||||
let install_shell: String = install_path.to_string_lossy().into_owned();
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let prompt = t("Service Administrator Prompt");
|
||||
let install_shell: String = install_path.to_string_lossy().into_owned();
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||
let command = format!(
|
||||
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||
r#"do shell script "sudo '{uninstall_shell}' && sudo '{install_shell}'" with administrator privileges"#
|
||||
);
|
||||
|
||||
logging!(debug, Type::Service, true, "install command: {}", command);
|
||||
log::debug!(target: "app", "command: {}", command);
|
||||
|
||||
let status = StdCommand::new("osascript")
|
||||
.args(vec!["-e", &command])
|
||||
@ -429,57 +156,9 @@ pub async fn install_service() -> Result<()> {
|
||||
status.code().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
let mut service_state = ServiceState::get();
|
||||
|
||||
// 检查是否允许重装
|
||||
if !service_state.can_reinstall() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"service reinstall rejected: cooldown period or max attempts reached"
|
||||
);
|
||||
bail!("Service reinstallation is rate limited. Please try again later.");
|
||||
}
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"failed to uninstall service: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service().await {
|
||||
Ok(_) => {
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
service_state.last_error = None;
|
||||
service_state.save()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.save()?;
|
||||
bail!(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// check the windows service status
|
||||
pub async fn check_service() -> Result<JsonResponse> {
|
||||
let url = format!("{SERVICE_URL}/get_clash");
|
||||
@ -498,78 +177,8 @@ pub async fn check_service() -> Result<JsonResponse> {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// check the service version
|
||||
pub async fn check_service_version() -> Result<String> {
|
||||
let url = format!("{SERVICE_URL}/version");
|
||||
let response = reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build()?
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to connect to the Clash Verge Service")?
|
||||
.json::<VersionJsonResponse>()
|
||||
.await
|
||||
.context("failed to parse the Clash Verge Service version response")?;
|
||||
|
||||
match response.data {
|
||||
Some(data) => Ok(data.version),
|
||||
None => bail!("service version not found in response"),
|
||||
}
|
||||
}
|
||||
|
||||
/// check if service needs to be reinstalled
|
||||
pub async fn check_service_needs_reinstall() -> bool {
|
||||
// 获取当前服务状态
|
||||
let service_state = ServiceState::get();
|
||||
|
||||
// 首先检查是否在冷却期或超过重装次数限制
|
||||
if !service_state.can_reinstall() {
|
||||
log::info!(target: "app", "service reinstall check: in cooldown period or max attempts reached");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 然后才检查版本和可用性
|
||||
match check_service_version().await {
|
||||
Ok(version) => {
|
||||
// 打印更详细的日志,方便排查问题
|
||||
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
|
||||
|
||||
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
||||
if needs_reinstall {
|
||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
|
||||
// 打印版本字符串的原始字节,确认没有隐藏字符
|
||||
log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
||||
log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配,无需重装");
|
||||
}
|
||||
|
||||
needs_reinstall
|
||||
}
|
||||
Err(err) => {
|
||||
// 检查服务是否可用,如果可用但版本检查失败,可能只是版本API有问题
|
||||
match is_service_running().await {
|
||||
Ok(true) => {
|
||||
log::info!(target: "app", "service is running but version check failed: {}", err);
|
||||
false // 服务在运行,不需要重装
|
||||
}
|
||||
_ => {
|
||||
log::info!(target: "app", "service is not running or unavailable");
|
||||
true // 服务不可用,需要重装
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 尝试使用现有服务启动核心,不进行重装
|
||||
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target:"app", "attempting to start core with existing service");
|
||||
|
||||
/// start the clash by service
|
||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
|
||||
@ -608,106 +217,6 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// start the clash by service
|
||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target: "app", "正在尝试通过服务启动核心");
|
||||
|
||||
// 先检查服务版本,不受冷却期限制
|
||||
let version_check = match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
|
||||
// 通过字节比较确保完全匹配
|
||||
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
||||
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
||||
false // 版本不匹配
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配");
|
||||
true // 版本匹配
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "无法获取服务版本: {}", err);
|
||||
false // 无法获取版本
|
||||
}
|
||||
};
|
||||
|
||||
// 先尝试直接启动服务,如果服务可用且版本匹配
|
||||
if version_check {
|
||||
if let Ok(true) = is_service_running().await {
|
||||
// 服务正在运行且版本匹配,直接使用
|
||||
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
}
|
||||
|
||||
// 强制执行版本检查,如果版本不匹配则重装
|
||||
if !version_check {
|
||||
log::info!(target: "app", "服务版本不匹配,尝试重装");
|
||||
|
||||
// 获取服务状态,检查是否可以重装
|
||||
let service_state = ServiceState::get();
|
||||
if !service_state.can_reinstall() {
|
||||
log::warn!(target: "app", "由于限制无法重装服务");
|
||||
// 尝试直接启动,即使版本不匹配
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
|
||||
return Ok(());
|
||||
} else {
|
||||
bail!("服务版本不匹配且无法重装,启动失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试重装
|
||||
log::info!(target: "app", "开始重装服务");
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
|
||||
// 尝试使用现有服务
|
||||
log::info!(target: "app", "尝试使用现有服务");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
// 重装成功,尝试启动
|
||||
log::info!(target: "app", "服务重装成功,尝试启动");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
// 检查服务状态
|
||||
match check_service().await {
|
||||
Ok(_) => {
|
||||
// 服务可访问但可能没有运行核心,尝试直接启动
|
||||
log::info!(target: "app", "服务可用但未运行核心,尝试启动");
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "服务检查失败: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 服务不可用或启动失败,检查是否需要重装
|
||||
if check_service_needs_reinstall().await {
|
||||
log::info!(target: "app", "服务需要重装");
|
||||
|
||||
// 尝试重装
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
|
||||
// 重装后再次尝试启动
|
||||
log::info!(target: "app", "服务重装完成,尝试启动核心");
|
||||
start_with_existing_service(config_file).await
|
||||
} else {
|
||||
// 不需要或不能重装,返回错误
|
||||
log::warn!(target: "app", "服务不可用且无法重装");
|
||||
bail!("Service is not available and cannot be reinstalled at this time")
|
||||
}
|
||||
}
|
||||
|
||||
/// stop the clash by service
|
||||
pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
let url = format!("{SERVICE_URL}/stop_clash");
|
||||
@ -721,48 +230,3 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查服务是否正在运行
|
||||
pub async fn is_service_running() -> Result<bool> {
|
||||
let resp = check_service().await?;
|
||||
|
||||
// 检查服务状态码和消息
|
||||
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
||||
logging!(debug, Type::Service, "Service is running");
|
||||
Ok(true)
|
||||
} else {
|
||||
logging!(debug, Type::Service, "Service is not running");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn is_service_available() -> Result<()> {
|
||||
let resp = check_service().await?;
|
||||
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
||||
logging!(debug, Type::Service, "Service is available");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 强制重装服务(用于UI中的修复服务按钮)
|
||||
pub async fn force_reinstall_service() -> Result<()> {
|
||||
log::info!(target: "app", "用户请求强制重装服务");
|
||||
|
||||
// 创建默认服务状态(重置所有限制)
|
||||
let service_state = ServiceState::default();
|
||||
service_state.save()?;
|
||||
|
||||
log::info!(target: "app", "已重置服务状态,开始执行重装");
|
||||
|
||||
// 执行重装
|
||||
match reinstall_service().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务重装成功");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "强制重装服务失败: {}", err);
|
||||
bail!("强制重装服务失败: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
use crate::core::handle::Handle;
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle::Handle,
|
||||
logging_error,
|
||||
utils::logging::Type,
|
||||
log_err,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
@ -25,11 +24,10 @@ pub struct Sysopt {
|
||||
#[cfg(target_os = "windows")]
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
|
||||
#[cfg(target_os = "linux")]
|
||||
static DEFAULT_BYPASS: &str =
|
||||
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
|
||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
fn get_bypass() -> String {
|
||||
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true);
|
||||
@ -127,7 +125,8 @@ impl Sysopt {
|
||||
if !sys_enable {
|
||||
return self.reset_sysproxy().await;
|
||||
}
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use crate::core::handle::Handle;
|
||||
use crate::utils::dirs;
|
||||
use anyhow::bail;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
@ -185,7 +184,8 @@ impl Sysopt {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use crate::core::handle::Handle;
|
||||
use crate::utils::dirs;
|
||||
use anyhow::bail;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
@ -221,50 +221,15 @@ impl Sysopt {
|
||||
let enable = enable.unwrap_or(false);
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
|
||||
log::info!(target: "app", "Setting auto launch to: {}", enable);
|
||||
|
||||
println!("enable: {}", enable);
|
||||
match enable {
|
||||
true => {
|
||||
let result = autostart_manager.enable();
|
||||
if let Err(ref e) = result {
|
||||
log::error!(target: "app", "Failed to enable auto launch: {}", e);
|
||||
} else {
|
||||
log::info!(target: "app", "Auto launch enabled successfully");
|
||||
}
|
||||
logging_error!(Type::System, true, result);
|
||||
}
|
||||
false => {
|
||||
let result = autostart_manager.disable();
|
||||
if let Err(ref e) = result {
|
||||
log::error!(target: "app", "Failed to disable auto launch: {}", e);
|
||||
} else {
|
||||
log::info!(target: "app", "Auto launch disabled successfully");
|
||||
}
|
||||
logging_error!(Type::System, true, result);
|
||||
}
|
||||
true => log_err!(autostart_manager.enable()),
|
||||
false => log_err!(autostart_manager.disable()),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取当前自启动的实际状态
|
||||
pub fn get_launch_status(&self) -> Result<bool> {
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
|
||||
match autostart_manager.is_enabled() {
|
||||
Ok(status) => {
|
||||
log::info!(target: "app", "Auto launch status: {}", status);
|
||||
Ok(status)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get auto launch status: {}", e);
|
||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn guard_proxy(&self) {
|
||||
let _lock = self.guard_state.lock();
|
||||
|
||||
@ -324,7 +289,7 @@ impl Sysopt {
|
||||
enable: true,
|
||||
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
|
||||
};
|
||||
logging_error!(Type::System, true, autoproxy.set_auto_proxy());
|
||||
log_err!(autoproxy.set_auto_proxy());
|
||||
} else {
|
||||
let sysproxy = Sysproxy {
|
||||
enable: true,
|
||||
@ -333,13 +298,14 @@ impl Sysopt {
|
||||
bypass: get_bypass(),
|
||||
};
|
||||
|
||||
logging_error!(Type::System, true, sysproxy.set_system_proxy());
|
||||
log_err!(sysproxy.set_system_proxy());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use crate::core::handle::Handle;
|
||||
use crate::utils::dirs;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
|
@ -1,34 +1,23 @@
|
||||
use crate::{
|
||||
config::Config, core::CoreManager, feat, logging, logging_error, utils::logging::Type,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::feat;
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
type TaskID = u64;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TimerTask {
|
||||
pub task_id: TaskID,
|
||||
pub interval_minutes: u64,
|
||||
#[allow(unused)]
|
||||
pub last_run: i64, // Timestamp of last execution
|
||||
}
|
||||
|
||||
pub struct Timer {
|
||||
/// cron manager
|
||||
pub delay_timer: Arc<RwLock<DelayTimer>>,
|
||||
delay_timer: Arc<Mutex<DelayTimer>>,
|
||||
|
||||
/// save the current state - using RwLock for better read concurrency
|
||||
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
|
||||
/// save the current state
|
||||
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
|
||||
|
||||
/// increment id - kept as mutex since it's just a counter
|
||||
pub timer_count: Arc<Mutex<TaskID>>,
|
||||
|
||||
/// Flag to mark if timer is initialized - atomic for better performance
|
||||
pub initialized: Arc<std::sync::atomic::AtomicBool>,
|
||||
/// increment id
|
||||
timer_count: Arc<Mutex<TaskID>>,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
@ -36,164 +25,68 @@ impl Timer {
|
||||
static TIMER: OnceCell<Timer> = OnceCell::new();
|
||||
|
||||
TIMER.get_or_init(|| Timer {
|
||||
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
|
||||
timer_map: Arc::new(RwLock::new(HashMap::new())),
|
||||
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
|
||||
timer_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
timer_count: Arc::new(Mutex::new(1)),
|
||||
initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Initialize timer with better error handling and atomic operations
|
||||
/// restore timer
|
||||
pub fn init(&self) -> Result<()> {
|
||||
// Use compare_exchange for thread-safe initialization check
|
||||
if self
|
||||
.initialized
|
||||
.compare_exchange(
|
||||
false,
|
||||
true,
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
logging!(debug, Type::Timer, "Timer already initialized, skipping...");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
logging!(info, Type::Timer, true, "Initializing timer...");
|
||||
|
||||
// Initialize timer tasks
|
||||
if let Err(e) = self.refresh() {
|
||||
// Reset initialization flag on error
|
||||
self.initialized
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
self.refresh()?;
|
||||
|
||||
let cur_timestamp = chrono::Local::now().timestamp();
|
||||
|
||||
// Collect profiles that need immediate update
|
||||
let profiles_to_update = if let Some(items) = Config::profiles().latest().get_items() {
|
||||
let timer_map = self.timer_map.lock();
|
||||
let delay_timer = self.delay_timer.lock();
|
||||
|
||||
if let Some(items) = Config::profiles().latest().get_items() {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let interval = item.option.as_ref()?.update_interval? as i64;
|
||||
// mins to seconds
|
||||
let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
|
||||
let updated = item.updated? as i64;
|
||||
let uid = item.uid.as_ref()?;
|
||||
|
||||
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
||||
Some(uid.clone())
|
||||
if interval > 0 && cur_timestamp - updated >= interval {
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Advance tasks outside of locks to minimize lock contention
|
||||
if !profiles_to_update.is_empty() {
|
||||
let timer_map = self.timer_map.read();
|
||||
let delay_timer = self.delay_timer.write();
|
||||
|
||||
for uid in profiles_to_update {
|
||||
if let Some(task) = timer_map.get(&uid) {
|
||||
logging!(info, Type::Timer, "Advancing task for uid: {}", uid);
|
||||
if let Err(e) = delay_timer.advance_task(task.task_id) {
|
||||
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
|
||||
.for_each(|item| {
|
||||
if let Some(uid) = item.uid.as_ref() {
|
||||
if let Some((task_id, _)) = timer_map.get(uid) {
|
||||
crate::log_err!(delay_timer.advance_task(*task_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logging!(info, Type::Timer, "Timer initialization completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refresh timer tasks with better error handling
|
||||
/// Correctly update all cron tasks
|
||||
pub fn refresh(&self) -> Result<()> {
|
||||
// Generate diff outside of lock to minimize lock contention
|
||||
let diff_map = self.gen_diff();
|
||||
|
||||
if diff_map.is_empty() {
|
||||
logging!(debug, Type::Timer, "No timer changes needed");
|
||||
return Ok(());
|
||||
}
|
||||
let mut timer_map = self.timer_map.lock();
|
||||
let mut delay_timer = self.delay_timer.lock();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"Refreshing {} timer tasks",
|
||||
diff_map.len()
|
||||
);
|
||||
|
||||
// Apply changes while holding locks
|
||||
let mut timer_map = self.timer_map.write();
|
||||
let mut delay_timer = self.delay_timer.write();
|
||||
|
||||
for (uid, diff) in diff_map {
|
||||
for (uid, diff) in diff_map.into_iter() {
|
||||
match diff {
|
||||
DiffFlag::Del(tid) => {
|
||||
timer_map.remove(&uid);
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"Failed to remove task {} for uid {}: {}",
|
||||
tid,
|
||||
uid,
|
||||
e
|
||||
);
|
||||
} else {
|
||||
logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid);
|
||||
}
|
||||
let _ = timer_map.remove(&uid);
|
||||
crate::log_err!(delay_timer.remove_task(tid));
|
||||
}
|
||||
DiffFlag::Add(tid, interval) => {
|
||||
let task = TimerTask {
|
||||
task_id: tid,
|
||||
interval_minutes: interval,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(uid.clone(), task);
|
||||
|
||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
|
||||
timer_map.remove(&uid); // Rollback on failure
|
||||
} else {
|
||||
logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid);
|
||||
}
|
||||
DiffFlag::Add(tid, val) => {
|
||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||
}
|
||||
DiffFlag::Mod(tid, interval) => {
|
||||
// Remove old task first
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"Failed to remove old task {} for uid {}: {}",
|
||||
tid,
|
||||
uid,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
// Then add the new one
|
||||
let task = TimerTask {
|
||||
task_id: tid,
|
||||
interval_minutes: interval,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(uid.clone(), task);
|
||||
|
||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||
logging_error!(Type::Timer, "Failed to update task for uid {}: {}", uid, e);
|
||||
timer_map.remove(&uid); // Rollback on failure
|
||||
} else {
|
||||
logging!(debug, Type::Timer, "Updated task {} for uid {}", tid, uid);
|
||||
}
|
||||
DiffFlag::Mod(tid, val) => {
|
||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||
crate::log_err!(delay_timer.remove_task(tid));
|
||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,17 +94,18 @@ impl Timer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate map of profile UIDs to update intervals
|
||||
/// generate a uid -> update_interval map
|
||||
fn gen_map(&self) -> HashMap<String, u64> {
|
||||
let mut new_map = HashMap::new();
|
||||
|
||||
if let Some(items) = Config::profiles().latest().get_items() {
|
||||
for item in items.iter() {
|
||||
if let Some(option) = item.option.as_ref() {
|
||||
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
|
||||
if interval > 0 {
|
||||
new_map.insert(uid.clone(), interval);
|
||||
}
|
||||
if item.option.is_some() {
|
||||
let option = item.option.as_ref().unwrap();
|
||||
let interval = option.update_interval.unwrap_or(0);
|
||||
|
||||
if interval > 0 {
|
||||
new_map.insert(item.uid.clone().unwrap(), interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -220,50 +114,39 @@ impl Timer {
|
||||
new_map
|
||||
}
|
||||
|
||||
/// Generate differences between current and new timer configuration
|
||||
/// generate the diff map for refresh
|
||||
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
||||
let mut diff_map = HashMap::new();
|
||||
|
||||
let timer_map = self.timer_map.lock();
|
||||
|
||||
let new_map = self.gen_map();
|
||||
let cur_map = &timer_map;
|
||||
|
||||
// Read lock for comparing current state
|
||||
let timer_map = self.timer_map.read();
|
||||
cur_map.iter().for_each(|(uid, (tid, val))| {
|
||||
let new_val = new_map.get(uid).unwrap_or(&0);
|
||||
|
||||
// Find tasks to modify or delete
|
||||
for (uid, task) in timer_map.iter() {
|
||||
match new_map.get(uid) {
|
||||
Some(&interval) if interval != task.interval_minutes => {
|
||||
// Task exists but interval changed
|
||||
diff_map.insert(uid.clone(), DiffFlag::Mod(task.task_id, interval));
|
||||
}
|
||||
None => {
|
||||
// Task no longer needed
|
||||
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
|
||||
}
|
||||
_ => {
|
||||
// Task exists with same interval, no change needed
|
||||
}
|
||||
if *new_val == 0 {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
|
||||
} else if new_val != val {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Find new tasks to add
|
||||
let mut next_id = *self.timer_count.lock();
|
||||
let mut count = self.timer_count.lock();
|
||||
|
||||
for (uid, &interval) in new_map.iter() {
|
||||
if !timer_map.contains_key(uid) {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Add(next_id, interval));
|
||||
next_id += 1;
|
||||
new_map.iter().for_each(|(uid, val)| {
|
||||
if cur_map.get(uid).is_none() {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
|
||||
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Update counter only if we added new tasks
|
||||
if next_id > *self.timer_count.lock() {
|
||||
*self.timer_count.lock() = next_id;
|
||||
}
|
||||
});
|
||||
|
||||
diff_map
|
||||
}
|
||||
|
||||
/// Add a timer task with better error handling
|
||||
/// add a cron task
|
||||
fn add_task(
|
||||
&self,
|
||||
delay_timer: &mut DelayTimer,
|
||||
@ -271,26 +154,12 @@ impl Timer {
|
||||
tid: TaskID,
|
||||
minutes: u64,
|
||||
) -> Result<()> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"Adding task: uid={}, id={}, interval={}min",
|
||||
uid,
|
||||
tid,
|
||||
minutes
|
||||
);
|
||||
|
||||
// Create a task with reasonable retries and backoff
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(tid)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_repeated_by_minutes(minutes)
|
||||
.spawn_async_routine(move || {
|
||||
let uid = uid.clone();
|
||||
async move {
|
||||
Self::async_task(uid).await;
|
||||
}
|
||||
})
|
||||
// .set_frequency_repeated_by_seconds(minutes) // for test
|
||||
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
delay_timer
|
||||
@ -300,42 +169,10 @@ impl Timer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Async task with better error handling and logging
|
||||
/// the task runner
|
||||
async fn async_task(uid: String) {
|
||||
let task_start = std::time::Instant::now();
|
||||
logging!(info, Type::Timer, "Running timer task for profile: {}", uid);
|
||||
|
||||
// Update profile
|
||||
let profile_result = feat::update_profile(uid.clone(), None).await;
|
||||
|
||||
match profile_result {
|
||||
Ok(_) => {
|
||||
// Update configuration
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
let duration = task_start.elapsed().as_millis();
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"Timer task completed successfully for uid: {} (took {}ms)",
|
||||
uid,
|
||||
duration
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
logging_error!(
|
||||
Type::Timer,
|
||||
"Failed to refresh config after profile update for uid {}: {}",
|
||||
uid,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging_error!(Type::Timer, "Failed to update profile uid {}: {}", uid, e);
|
||||
}
|
||||
}
|
||||
log::info!(target: "app", "running timer task `{uid}`");
|
||||
crate::log_err!(feat::update_profile(uid, None).await);
|
||||
}
|
||||
}
|
||||
|
||||
|
432
src-tauri/src/core/tray.rs
Normal file
432
src-tauri/src/core/tray.rs
Normal file
@ -0,0 +1,432 @@
|
||||
use crate::{
|
||||
cmds,
|
||||
config::Config,
|
||||
feat, t,
|
||||
utils::{
|
||||
dirs,
|
||||
resolve::{self, VERSION},
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use tauri::AppHandle;
|
||||
use tauri::{
|
||||
menu::CheckMenuItem,
|
||||
tray::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId},
|
||||
};
|
||||
use tauri::{
|
||||
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
Wry,
|
||||
};
|
||||
|
||||
use super::handle;
|
||||
pub struct Tray {}
|
||||
|
||||
impl Tray {
|
||||
pub fn create_systray() -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let tray_incon_id = TrayIconId::new("main");
|
||||
let tray = app_handle.tray_by_id(&tray_incon_id).unwrap();
|
||||
|
||||
tray.on_tray_icon_event(|_, event| {
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Right,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"main_window" => resolve::create_window(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"main_window" => resolve::create_window(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
tray.on_menu_event(on_menu_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_part() -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let use_zh = { Config::verge().latest().language == Some("zh".into()) };
|
||||
let version = VERSION.get().unwrap();
|
||||
let mode = {
|
||||
Config::clash()
|
||||
.latest()
|
||||
.0
|
||||
.get("mode")
|
||||
.map(|val| val.as_str().unwrap_or("rule"))
|
||||
.unwrap_or("rule")
|
||||
.to_owned()
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
|
||||
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
|
||||
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
|
||||
let _ = tray.set_menu(Some(create_tray_menu(
|
||||
&app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
)?));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut use_custom_icon = false;
|
||||
#[allow(unused)]
|
||||
let mut indication_icon = if *system_proxy && !*tun_mode {
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut icon = match tray_icon.as_str() {
|
||||
"colorful" => {
|
||||
use_custom_icon = true;
|
||||
include_bytes!("../../icons/tray-icon-sys.ico").to_vec()
|
||||
}
|
||||
_ => include_bytes!("../../icons/tray-icon-sys-mono.ico").to_vec(),
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let mut icon = include_bytes!("../../icons/tray-icon-sys.ico").to_vec();
|
||||
if *sysproxy_tray_icon {
|
||||
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
||||
let png_path = icon_dir_path.join("sysproxy.png");
|
||||
let ico_path = icon_dir_path.join("sysproxy.ico");
|
||||
if ico_path.exists() {
|
||||
icon = std::fs::read(ico_path).unwrap();
|
||||
} else if png_path.exists() {
|
||||
icon = std::fs::read(png_path).unwrap();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use_custom_icon = true;
|
||||
}
|
||||
}
|
||||
icon
|
||||
} else if *tun_mode {
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut icon = match tray_icon.as_str() {
|
||||
"colorful" => {
|
||||
use_custom_icon = true;
|
||||
include_bytes!("../../icons/tray-icon-tun.ico").to_vec()
|
||||
}
|
||||
_ => include_bytes!("../../icons/tray-icon-tun-mono.ico").to_vec(),
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let mut icon = include_bytes!("../../icons/tray-icon-tun.ico").to_vec();
|
||||
if *tun_tray_icon {
|
||||
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
||||
let png_path = icon_dir_path.join("tun.png");
|
||||
let ico_path = icon_dir_path.join("tun.ico");
|
||||
if ico_path.exists() {
|
||||
icon = std::fs::read(ico_path).unwrap();
|
||||
} else if png_path.exists() {
|
||||
icon = std::fs::read(png_path).unwrap();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use_custom_icon = true;
|
||||
}
|
||||
}
|
||||
icon
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut icon = match tray_icon.as_str() {
|
||||
"colorful" => {
|
||||
use_custom_icon = true;
|
||||
include_bytes!("../../icons/tray-icon.ico").to_vec()
|
||||
}
|
||||
_ => include_bytes!("../../icons/tray-icon-mono.ico").to_vec(),
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let mut icon = include_bytes!("../../icons/tray-icon.ico").to_vec();
|
||||
if *common_tray_icon {
|
||||
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
||||
let png_path = icon_dir_path.join("common.png");
|
||||
let ico_path = icon_dir_path.join("common.ico");
|
||||
if ico_path.exists() {
|
||||
icon = std::fs::read(ico_path).unwrap();
|
||||
} else if png_path.exists() {
|
||||
icon = std::fs::read(png_path).unwrap();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use_custom_icon = true;
|
||||
}
|
||||
}
|
||||
icon
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if use_custom_icon {
|
||||
let _ = tray.set_icon_as_template(false);
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&indication_icon)?));
|
||||
} else {
|
||||
let _ = tray.set_icon_as_template(true);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&indication_icon)?));
|
||||
|
||||
let switch_map = {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(true, "on");
|
||||
map.insert(false, "off");
|
||||
map
|
||||
};
|
||||
|
||||
let mut current_profile_name = "None".to_string();
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
if let Some(current_profile_uid) = profiles.get_current() {
|
||||
let current_profile = profiles.get_item(¤t_profile_uid);
|
||||
current_profile_name = match ¤t_profile.unwrap().name {
|
||||
Some(profile_name) => profile_name.to_string(),
|
||||
None => current_profile_name,
|
||||
};
|
||||
};
|
||||
|
||||
let _ = tray.set_tooltip(Some(&format!(
|
||||
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||
t!("SysProxy", "系统代理", use_zh),
|
||||
switch_map[system_proxy],
|
||||
t!("TUN", "Tun模式", use_zh),
|
||||
switch_map[tun_mode],
|
||||
t!("Profile", "当前订阅", use_zh),
|
||||
current_profile_name
|
||||
)));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tray_menu(
|
||||
app_handle: &AppHandle,
|
||||
mode: Option<&str>,
|
||||
system_proxy_enabled: bool,
|
||||
tun_mode_enabled: bool,
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let mode = mode.unwrap_or("");
|
||||
let use_zh = { Config::verge().latest().language == Some("zh".into()) };
|
||||
let version = VERSION.get().unwrap();
|
||||
|
||||
let open_window = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_window",
|
||||
t!("Dashboard", "打开面板", use_zh),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let rule_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"rule_mode",
|
||||
t!("Rule Mode", "规则模式", use_zh),
|
||||
true,
|
||||
mode == "rule",
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let global_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"global_mode",
|
||||
t!("Global Mode", "全局模式", use_zh),
|
||||
true,
|
||||
mode == "global",
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let direct_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"direct_mode",
|
||||
t!("Direct Mode", "直连模式", use_zh),
|
||||
true,
|
||||
mode == "direct",
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let system_proxy = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"system_proxy",
|
||||
t!("System Proxy", "系统代理", use_zh),
|
||||
true,
|
||||
system_proxy_enabled,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tun_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"tun_mode",
|
||||
t!("TUN Mode", "Tun模式", use_zh),
|
||||
true,
|
||||
tun_mode_enabled,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let copy_env = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"copy_env",
|
||||
t!("Copy Env", "复制环境变量", use_zh),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_app_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_app_dir",
|
||||
t!("Conf Dir", "配置目录", use_zh),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_core_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_core_dir",
|
||||
t!("Core Dir", "内核目录", use_zh),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_logs_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_logs_dir",
|
||||
t!("Logs Dir", "日志目录", use_zh),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
let open_dir = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"open_dir",
|
||||
t!("Open Dir", "打开目录", use_zh),
|
||||
true,
|
||||
&[open_app_dir, open_core_dir, open_logs_dir],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let restart_clash = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_clash",
|
||||
t!("Restart Clash Core", "重启Clash内核", use_zh),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let restart_app = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_app",
|
||||
t!("Restart App", "重启App", use_zh),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let app_version = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"app_version",
|
||||
format!("Version {version}"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let more = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"more",
|
||||
t!("More", "更多", use_zh),
|
||||
true,
|
||||
&[restart_clash, restart_app, app_version],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let quit = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"quit",
|
||||
t!("Quit", "退出", use_zh),
|
||||
true,
|
||||
Some("CmdOrControl+Q"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app_handle)
|
||||
.items(&[
|
||||
open_window,
|
||||
separator,
|
||||
rule_mode,
|
||||
global_mode,
|
||||
direct_mode,
|
||||
separator,
|
||||
system_proxy,
|
||||
tun_mode,
|
||||
copy_env,
|
||||
open_dir,
|
||||
more,
|
||||
separator,
|
||||
quit,
|
||||
])
|
||||
.build()
|
||||
.unwrap();
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
match event.id.as_ref() {
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5];
|
||||
println!("change mode to: {}", mode);
|
||||
feat::change_clash_mode(mode.into());
|
||||
}
|
||||
"open_window" => resolve::create_window(),
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
||||
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
||||
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"quit" => {
|
||||
println!("quit");
|
||||
feat::quit(Some(0));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
@ -1,697 +0,0 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::{
|
||||
cmd,
|
||||
config::Config,
|
||||
feat,
|
||||
module::{lightweight::entry_lightweight_mode, mihomo::Rate},
|
||||
resolve,
|
||||
utils::{dirs::find_target_icons, i18n::t, logging::Type, resolve::VERSION},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
#[cfg(target_os = "macos")]
|
||||
use futures::StreamExt;
|
||||
#[cfg(target_os = "macos")]
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(target_os = "macos")]
|
||||
use parking_lot::RwLock;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use speed_rate::{SpeedRate, Traffic};
|
||||
use std::fs;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::Arc;
|
||||
use tauri::{
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
||||
App, AppHandle, Wry,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use super::handle;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TrayState {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub struct Tray {
|
||||
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||
is_subscribed: Arc<RwLock<bool>>,
|
||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub struct Tray {}
|
||||
|
||||
impl TrayState {
|
||||
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
|
||||
if is_common_tray_icon {
|
||||
if let Some(common_icon_path) = find_target_icons("common").unwrap() {
|
||||
let icon_data = fs::read(common_icon_path).unwrap();
|
||||
return (true, icon_data);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let tray_icon_colorful = verge.tray_icon.unwrap_or("monochrome".to_string());
|
||||
if tray_icon_colorful == "monochrome" {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon.ico").to_vec(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon.ico").to_vec(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
|
||||
if is_sysproxy_tray_icon {
|
||||
if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() {
|
||||
let icon_data = fs::read(sysproxy_icon_path).unwrap();
|
||||
return (true, icon_data);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
if tray_icon_colorful == "monochrome" {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_tun_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
|
||||
if is_tun_tray_icon {
|
||||
if let Some(tun_icon_path) = find_target_icons("tun").unwrap() {
|
||||
let icon_data = fs::read(tun_icon_path).unwrap();
|
||||
return (true, icon_data);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
if tray_icon_colorful == "monochrome" {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
|
||||
)
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tray {
|
||||
pub fn global() -> &'static Tray {
|
||||
static TRAY: OnceCell<Tray> = OnceCell::new();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return TRAY.get_or_init(|| Tray {
|
||||
speed_rate: Arc::new(Mutex::new(None)),
|
||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||
is_subscribed: Arc::new(RwLock::new(false)),
|
||||
rate_cache: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
return TRAY.get_or_init(|| Tray {});
|
||||
}
|
||||
|
||||
pub fn init(&self) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut speed_rate = self.speed_rate.lock();
|
||||
*speed_rate = Some(SpeedRate::new());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_systray(&self, app: &App) -> Result<()> {
|
||||
let mut builder = TrayIconBuilder::with_id("main")
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.icon_as_template(false);
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
{
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
if tray_event.as_str() != "tray_menu" {
|
||||
builder = builder.show_menu_on_left_click(false);
|
||||
}
|
||||
}
|
||||
|
||||
let tray = builder.build(app)?;
|
||||
|
||||
tray.on_tray_icon_event(|_, event| {
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
log::debug!(target: "app","tray event: {:?}", tray_event);
|
||||
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"main_window" => resolve::create_window(true),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
tray.on_menu_event(on_menu_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘点击行为
|
||||
pub fn update_click_behavior(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
match tray_event.as_str() {
|
||||
"tray_menu" => tray.set_show_menu_on_left_click(true)?,
|
||||
_ => tray.set_show_menu_on_left_click(false)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘菜单
|
||||
pub fn update_menu(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let verge = Config::verge().latest().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
let mode = {
|
||||
Config::clash()
|
||||
.latest()
|
||||
.0
|
||||
.get("mode")
|
||||
.map(|val| val.as_str().unwrap_or("rule"))
|
||||
.unwrap_or("rule")
|
||||
.to_owned()
|
||||
};
|
||||
let profile_uid_and_name = Config::profiles()
|
||||
.data()
|
||||
.all_profile_uid_and_name()
|
||||
.unwrap_or_default();
|
||||
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
let _ = tray.set_menu(Some(create_tray_menu(
|
||||
&app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
profile_uid_and_name,
|
||||
)?));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘图标
|
||||
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
|
||||
let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
(true, true) => TrayState::get_tun_tray_icon(),
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
||||
(false, true) => TrayState::get_tun_tray_icon(),
|
||||
(false, false) => TrayState::get_common_tray_icon(),
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true);
|
||||
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
||||
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
let is_colorful = colorful == "colorful";
|
||||
|
||||
if !enable_tray_speed {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rate = if let Some(rate) = rate {
|
||||
Some(rate)
|
||||
} else {
|
||||
let guard = self.speed_rate.lock();
|
||||
if let Some(rate) = guard.as_ref().unwrap().get_curent_rate() {
|
||||
Some(rate)
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
};
|
||||
|
||||
let mut rate_guard = self.rate_cache.lock();
|
||||
if *rate_guard != rate {
|
||||
*rate_guard = rate;
|
||||
|
||||
let bytes = if enable_tray_icon {
|
||||
Some(icon_bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rate = rate_guard.as_ref();
|
||||
let rate_bytes = SpeedRate::add_speed_text(is_custom_icon, bytes, rate).unwrap();
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新托盘提示
|
||||
pub fn update_tooltip(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let version = VERSION.get().unwrap();
|
||||
|
||||
let verge = Config::verge().latest().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
let switch_map = {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(true, "on");
|
||||
map.insert(false, "off");
|
||||
map
|
||||
};
|
||||
|
||||
let mut current_profile_name = "None".to_string();
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
if let Some(current_profile_uid) = profiles.get_current() {
|
||||
let current_profile = profiles.get_item(¤t_profile_uid);
|
||||
current_profile_name = match ¤t_profile.unwrap().name {
|
||||
Some(profile_name) => profile_name.to_string(),
|
||||
None => current_profile_name,
|
||||
};
|
||||
};
|
||||
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
let _ = tray.set_tooltip(Some(&format!(
|
||||
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||
t("SysProxy"),
|
||||
switch_map[system_proxy],
|
||||
t("TUN"),
|
||||
switch_map[tun_mode],
|
||||
t("Profile"),
|
||||
current_profile_name
|
||||
)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_part(&self) -> Result<()> {
|
||||
self.update_menu()?;
|
||||
self.update_icon(None)?;
|
||||
self.update_tooltip()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 订阅流量数据
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn subscribe_traffic(&self) -> Result<()> {
|
||||
log::info!(target: "app", "subscribe traffic");
|
||||
|
||||
// 如果已经订阅,先取消订阅
|
||||
if *self.is_subscribed.read() {
|
||||
self.unsubscribe_traffic();
|
||||
}
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = broadcast::channel(1);
|
||||
*self.shutdown_tx.write() = Some(shutdown_tx);
|
||||
*self.is_subscribed.write() = true;
|
||||
|
||||
let speed_rate = Arc::clone(&self.speed_rate);
|
||||
let is_subscribed = Arc::clone(&self.is_subscribed);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut shutdown = shutdown_rx;
|
||||
|
||||
'outer: loop {
|
||||
match Traffic::get_traffic_stream().await {
|
||||
Ok(mut stream) => loop {
|
||||
tokio::select! {
|
||||
Some(traffic) = stream.next() => {
|
||||
if let Ok(traffic) = traffic {
|
||||
let guard = speed_rate.lock();
|
||||
let enable_tray_speed: bool = Config::verge().latest().enable_tray_speed.unwrap_or(true);
|
||||
if !enable_tray_speed {
|
||||
continue;
|
||||
}
|
||||
if let Some(sr) = guard.as_ref() {
|
||||
if let Some(rate) = sr.update_and_check_changed(traffic.up, traffic.down) {
|
||||
let _ = Tray::global().update_icon(Some(rate));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => break 'outer,
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get traffic stream: {}", e);
|
||||
// 如果获取流失败,等待一段时间后重试
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
// 检查是否应该继续重试
|
||||
if !*is_subscribed.read() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 取消订阅 traffic 数据
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn unsubscribe_traffic(&self) {
|
||||
log::info!(target: "app", "unsubscribe traffic");
|
||||
*self.is_subscribed.write() = false;
|
||||
if let Some(tx) = self.shutdown_tx.write().take() {
|
||||
drop(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tray_menu(
|
||||
app_handle: &AppHandle,
|
||||
mode: Option<&str>,
|
||||
system_proxy_enabled: bool,
|
||||
tun_mode_enabled: bool,
|
||||
profile_uid_and_name: Vec<(String, String)>,
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let mode = mode.unwrap_or("");
|
||||
let version = VERSION.get().unwrap();
|
||||
let hotkeys = Config::verge()
|
||||
.latest()
|
||||
.hotkeys
|
||||
.as_ref()
|
||||
.map(|h| {
|
||||
h.iter()
|
||||
.filter_map(|item| {
|
||||
let mut parts = item.split(',');
|
||||
match (parts.next(), parts.next()) {
|
||||
(Some(func), Some(key)) => Some((func.to_string(), key.to_string())),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<std::collections::HashMap<String, String>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
|
||||
.iter()
|
||||
.map(|(profile_uid, profile_name)| {
|
||||
let is_current_profile = Config::profiles()
|
||||
.data()
|
||||
.is_current_profile_index(profile_uid.to_string());
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
format!("profiles_{}", profile_uid),
|
||||
t(profile_name),
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
|
||||
let open_window = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_window",
|
||||
t("Dashboard"),
|
||||
true,
|
||||
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let rule_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"rule_mode",
|
||||
t("Rule Mode"),
|
||||
true,
|
||||
mode == "rule",
|
||||
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let global_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"global_mode",
|
||||
t("Global Mode"),
|
||||
true,
|
||||
mode == "global",
|
||||
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let direct_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"direct_mode",
|
||||
t("Direct Mode"),
|
||||
true,
|
||||
mode == "direct",
|
||||
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let profiles = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"profiles",
|
||||
t("Profiles"),
|
||||
true,
|
||||
&profile_menu_items,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let system_proxy = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"system_proxy",
|
||||
t("System Proxy"),
|
||||
true,
|
||||
system_proxy_enabled,
|
||||
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tun_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"tun_mode",
|
||||
t("TUN Mode"),
|
||||
true,
|
||||
tun_mode_enabled,
|
||||
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let lighteweight_mode = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"entry_lightweight_mode",
|
||||
t("LightWeight Mode"),
|
||||
true,
|
||||
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let copy_env =
|
||||
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
||||
|
||||
let open_app_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_app_dir",
|
||||
t("Conf Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_core_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_core_dir",
|
||||
t("Core Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_logs_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_logs_dir",
|
||||
t("Logs Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_dir = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"open_dir",
|
||||
t("Open Dir"),
|
||||
true,
|
||||
&[open_app_dir, open_core_dir, open_logs_dir],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let restart_clash = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_clash",
|
||||
t("Restart Clash Core"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let restart_app = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_app",
|
||||
t("Restart App"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let app_version = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"app_version",
|
||||
format!("{} {version}", t("Verge Version")),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let more = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"more",
|
||||
t("More"),
|
||||
true,
|
||||
&[restart_clash, restart_app, app_version],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let quit =
|
||||
&MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q")).unwrap();
|
||||
|
||||
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app_handle)
|
||||
.items(&[
|
||||
open_window,
|
||||
separator,
|
||||
rule_mode,
|
||||
global_mode,
|
||||
direct_mode,
|
||||
separator,
|
||||
profiles,
|
||||
separator,
|
||||
system_proxy,
|
||||
tun_mode,
|
||||
separator,
|
||||
lighteweight_mode,
|
||||
copy_env,
|
||||
open_dir,
|
||||
more,
|
||||
separator,
|
||||
quit,
|
||||
])
|
||||
.build()
|
||||
.unwrap();
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
match event.id.as_ref() {
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5];
|
||||
println!("change mode to: {}", mode);
|
||||
feat::change_clash_mode(mode.into());
|
||||
}
|
||||
"open_window" => resolve::create_window(true),
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => crate::logging_error!(Type::Cmd, true, cmd::open_app_dir()),
|
||||
"open_core_dir" => crate::logging_error!(Type::Cmd, true, cmd::open_core_dir()),
|
||||
"open_logs_dir" => crate::logging_error!(Type::Cmd, true, cmd::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"entry_lightweight_mode" => entry_lightweight_mode(),
|
||||
"quit" => {
|
||||
feat::quit(Some(0));
|
||||
}
|
||||
id if id.starts_with("profiles_") => {
|
||||
let profile_index = &id["profiles_".len()..];
|
||||
feat::toggle_proxy_profile(profile_index.into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
@ -1,285 +0,0 @@
|
||||
use crate::{
|
||||
module::mihomo::{MihomoManager, Rate},
|
||||
utils::help::format_bytes_speed,
|
||||
};
|
||||
use ab_glyph::FontArc;
|
||||
use anyhow::Result;
|
||||
use futures::Stream;
|
||||
use image::{GenericImageView, Rgba, RgbaImage};
|
||||
use imageproc::drawing::draw_text_mut;
|
||||
use parking_lot::Mutex;
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
use tokio_tungstenite::tungstenite::{http, Message};
|
||||
use tungstenite::client::IntoClientRequest;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpeedRate {
|
||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||
last_update: Arc<Mutex<std::time::Instant>>,
|
||||
}
|
||||
|
||||
impl SpeedRate {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
|
||||
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option<Rate> {
|
||||
let mut rates = self.rate.lock();
|
||||
let mut last_update = self.last_update.lock();
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
// 限制更新频率为每秒最多2次(500ms)
|
||||
if now.duration_since(*last_update).as_millis() < 500 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (current, previous) = &mut *rates;
|
||||
|
||||
// Avoid unnecessary float conversions for small value checks
|
||||
let should_update = if current.up < 1000 && down < 1000 {
|
||||
// For small values, always update to ensure accuracy
|
||||
current.up != up || current.down != down
|
||||
} else {
|
||||
// For larger values, use integer math to check for >5% change
|
||||
// Multiply by 20 instead of dividing by 0.05 to avoid floating point
|
||||
let up_threshold = current.up / 20;
|
||||
let down_threshold = current.down / 20;
|
||||
|
||||
(up > current.up && up - current.up > up_threshold)
|
||||
|| (up < current.up && current.up - up > up_threshold)
|
||||
|| (down > current.down && down - current.down > down_threshold)
|
||||
|| (down < current.down && current.down - down > down_threshold)
|
||||
};
|
||||
|
||||
if !should_update {
|
||||
return None;
|
||||
}
|
||||
|
||||
*previous = current.clone();
|
||||
current.up = up;
|
||||
current.down = down;
|
||||
*last_update = now;
|
||||
|
||||
if previous != current {
|
||||
Some(current.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_curent_rate(&self) -> Option<Rate> {
|
||||
let rates = self.rate.lock();
|
||||
let (current, _) = &*rates;
|
||||
Some(current.clone())
|
||||
}
|
||||
|
||||
// 分离图标加载和速率渲染
|
||||
pub fn add_speed_text(
|
||||
is_custom_icon: bool,
|
||||
icon_bytes: Option<Vec<u8>>,
|
||||
rate: Option<&Rate>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
|
||||
|
||||
let (mut icon_width, mut icon_height) = (0, 256);
|
||||
let icon_image = if let Some(bytes) = icon_bytes.clone() {
|
||||
let icon_image = image::load_from_memory(&bytes)?;
|
||||
icon_width = icon_image.width();
|
||||
icon_height = icon_image.height();
|
||||
icon_image
|
||||
} else {
|
||||
// 返回一个空的 RGBA 图像
|
||||
image::DynamicImage::new_rgba8(0, 0)
|
||||
};
|
||||
|
||||
let total_width = match (is_custom_icon, icon_bytes.is_some()) {
|
||||
(true, true) => 510,
|
||||
(true, false) => 740,
|
||||
(false, false) => 740,
|
||||
(false, true) => icon_width + 740,
|
||||
};
|
||||
|
||||
// println!(
|
||||
// "icon_height: {}, icon_wight: {}, total_width: {}",
|
||||
// icon_height, icon_width, total_width
|
||||
// );
|
||||
|
||||
// 创建新的透明画布
|
||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||
|
||||
// 将原始图标绘制到新画布的左侧
|
||||
if icon_bytes.is_some() {
|
||||
for y in 0..icon_height {
|
||||
for x in 0..icon_width {
|
||||
let pixel = icon_image.get_pixel(x, y);
|
||||
combined_image.put_pixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_colorful = if let Some(bytes) = icon_bytes.clone() {
|
||||
!crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 选择文本颜色
|
||||
let (text_color, shadow_color) = if is_colorful {
|
||||
(
|
||||
Rgba([144u8, 144u8, 144u8, 255u8]),
|
||||
// Rgba([255u8, 255u8, 255u8, 128u8]),
|
||||
Rgba([0u8, 0u8, 0u8, 128u8]),
|
||||
)
|
||||
// (
|
||||
// Rgba([160u8, 160u8, 160u8, 255u8]),
|
||||
// // Rgba([255u8, 255u8, 255u8, 128u8]),
|
||||
// Rgba([0u8, 0u8, 0u8, 255u8]),
|
||||
// )
|
||||
} else {
|
||||
(
|
||||
Rgba([255u8, 255u8, 255u8, 255u8]),
|
||||
Rgba([0u8, 0u8, 0u8, 128u8]),
|
||||
)
|
||||
};
|
||||
// 减小字体大小以适应文本区域
|
||||
let font_data = include_bytes!("../../../assets/fonts/SF-Pro.ttf");
|
||||
let font = FontArc::try_from_vec(font_data.to_vec()).unwrap();
|
||||
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
|
||||
let scale = ab_glyph::PxScale::from(font_size);
|
||||
|
||||
// 使用更简洁的速率格式
|
||||
let up_text = format!("↑ {}", format_bytes_speed(rate.up));
|
||||
let down_text = format!("↓ {}", format_bytes_speed(rate.down));
|
||||
|
||||
// For test rate display
|
||||
// let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024));
|
||||
|
||||
// 计算文本位置,确保垂直间距合适
|
||||
// 修改文本位置为居右显示
|
||||
// 计算右对齐的文本位置
|
||||
// let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
|
||||
// let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
|
||||
// let up_text_x = total_width - up_text_width;
|
||||
// let down_text_x = total_width - down_text_width;
|
||||
|
||||
// 计算左对齐的文本位置
|
||||
let (up_text_x, down_text_x) = {
|
||||
if is_custom_icon || icon_bytes.is_some() {
|
||||
let text_left_offset = 30;
|
||||
let left_begin = icon_width + text_left_offset;
|
||||
(left_begin, left_begin)
|
||||
} else {
|
||||
(icon_width, icon_width)
|
||||
}
|
||||
};
|
||||
|
||||
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
|
||||
let text_height = font_size as i32;
|
||||
let total_text_height = text_height * 2;
|
||||
let up_y = (icon_height as i32 - total_text_height) / 2;
|
||||
let down_y = up_y + text_height;
|
||||
|
||||
// 绘制速率文本(先阴影后文字)
|
||||
let shadow_offset = 1;
|
||||
|
||||
// 绘制上行速率
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
shadow_color,
|
||||
up_text_x as i32 + shadow_offset,
|
||||
up_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
);
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
text_color,
|
||||
up_text_x as i32,
|
||||
up_y,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
);
|
||||
|
||||
// 绘制下行速率
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
shadow_color,
|
||||
down_text_x as i32 + shadow_offset,
|
||||
down_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
);
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
text_color,
|
||||
down_text_x as i32,
|
||||
down_y,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
);
|
||||
|
||||
// 将结果转换为 PNG 数据
|
||||
let mut bytes = Vec::new();
|
||||
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Traffic {
|
||||
pub up: u64,
|
||||
pub down: u64,
|
||||
}
|
||||
|
||||
impl Traffic {
|
||||
pub async fn get_traffic_stream() -> Result<impl Stream<Item = Result<Traffic, anyhow::Error>>>
|
||||
{
|
||||
use futures::stream::{self, StreamExt};
|
||||
use std::time::Duration;
|
||||
|
||||
let stream = Box::pin(
|
||||
stream::unfold((), |_| async {
|
||||
loop {
|
||||
let (url, token) = MihomoManager::get_traffic_ws_url();
|
||||
let mut request = url.into_client_request().unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.insert(http::header::AUTHORIZATION, token);
|
||||
|
||||
match tokio_tungstenite::connect_async(request).await {
|
||||
Ok((ws_stream, _)) => {
|
||||
log::info!(target: "app", "traffic ws connection established");
|
||||
return Some((
|
||||
ws_stream.map(|msg| {
|
||||
msg.map_err(anyhow::Error::from).and_then(|msg: Message| {
|
||||
let data = msg.into_text()?;
|
||||
let json: serde_json::Value = serde_json::from_str(&data)?;
|
||||
Ok(Traffic {
|
||||
up: json["up"].as_u64().unwrap_or(0),
|
||||
down: json["down"].as_u64().unwrap_or(0),
|
||||
})
|
||||
})
|
||||
}),
|
||||
(),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "traffic ws connection failed: {e}");
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
@ -5,10 +5,17 @@ mod script;
|
||||
pub mod seq;
|
||||
mod tun;
|
||||
|
||||
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
|
||||
use crate::{config::Config, utils::tmpl};
|
||||
use self::chain::*;
|
||||
use self::field::*;
|
||||
use self::merge::*;
|
||||
use self::script::*;
|
||||
use self::seq::*;
|
||||
use self::tun::*;
|
||||
use crate::config::Config;
|
||||
use crate::utils::tmpl;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
type ResultLog = Vec<(String, String)>;
|
||||
|
||||
@ -18,7 +25,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// config.yaml 的订阅
|
||||
let clash_config = { Config::clash().latest().0.clone() };
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
@ -27,7 +34,6 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||
verge.verge_socks_enabled.unwrap_or(false),
|
||||
verge.verge_http_enabled.unwrap_or(false),
|
||||
verge.enable_dns_settings.unwrap_or(false),
|
||||
)
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@ -256,27 +262,6 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
config = use_tun(config, enable_tun).await;
|
||||
config = use_sort(config);
|
||||
|
||||
// 应用独立的DNS配置(如果启用)
|
||||
if enable_dns_settings {
|
||||
use crate::utils::dirs;
|
||||
use std::fs;
|
||||
|
||||
// 尝试读取dns_config.yaml
|
||||
if let Ok(app_dir) = dirs::app_home_dir() {
|
||||
let dns_path = app_dir.join("dns_config.yaml");
|
||||
|
||||
if dns_path.exists() {
|
||||
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
|
||||
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
||||
// 将DNS配置合并到最终配置中
|
||||
config.insert("dns".into(), dns_config.into());
|
||||
log::info!(target: "app", "apply dns_config.yaml");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut exists_set = HashSet::new();
|
||||
exists_set.extend(exists_keys);
|
||||
exists_keys = exists_set.into_iter().collect();
|
||||
|
@ -1,155 +1,55 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::{Mapping, Sequence, Value};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SeqMap {
|
||||
pub prepend: Sequence,
|
||||
pub append: Sequence,
|
||||
pub delete: Vec<String>,
|
||||
prepend: Sequence,
|
||||
append: Sequence,
|
||||
delete: Sequence,
|
||||
}
|
||||
|
||||
pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
let SeqMap {
|
||||
prepend,
|
||||
append,
|
||||
delete,
|
||||
} = seq;
|
||||
pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
|
||||
let mut prepend = seq_map.prepend;
|
||||
let append = seq_map.append;
|
||||
let delete = seq_map.delete;
|
||||
|
||||
let mut new_seq = Sequence::new();
|
||||
new_seq.extend(prepend);
|
||||
let origin_seq = config.get(name).map_or(Sequence::default(), |val| {
|
||||
val.as_sequence().unwrap_or(&Sequence::default()).clone()
|
||||
});
|
||||
let mut seq = origin_seq.clone();
|
||||
|
||||
if let Some(Value::Sequence(origin)) = config.get(field) {
|
||||
// Filter out deleted items
|
||||
let filtered: Sequence = origin
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
if let Value::String(s) = item {
|
||||
!delete.contains(s)
|
||||
} else if let Value::Mapping(m) = item {
|
||||
if let Some(Value::String(name)) = m.get("name") {
|
||||
!delete.contains(name)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
new_seq.extend(filtered);
|
||||
}
|
||||
|
||||
new_seq.extend(append);
|
||||
config.insert(Value::String(field.into()), Value::Sequence(new_seq));
|
||||
|
||||
// If this is proxies field, we also need to filter proxy-groups
|
||||
if field == "proxies" {
|
||||
if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") {
|
||||
let mut new_groups = Sequence::new();
|
||||
for group in groups {
|
||||
if let Value::Mapping(group_map) = group {
|
||||
let mut new_group = group_map.clone();
|
||||
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
|
||||
let filtered_proxies: Sequence = proxies
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
if let Value::String(name) = p {
|
||||
!delete.contains(name)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
new_group.insert(
|
||||
Value::String("proxies".into()),
|
||||
Value::Sequence(filtered_proxies),
|
||||
);
|
||||
}
|
||||
new_groups.push(Value::Mapping(new_group));
|
||||
} else {
|
||||
new_groups.push(group.clone());
|
||||
}
|
||||
}
|
||||
config.insert(
|
||||
Value::String("proxy-groups".into()),
|
||||
Value::Sequence(new_groups),
|
||||
);
|
||||
let mut delete_names = Vec::new();
|
||||
for item in delete {
|
||||
let item = item.clone();
|
||||
if let Some(name) = if item.is_string() {
|
||||
Some(item)
|
||||
} else {
|
||||
item.get("name").cloned()
|
||||
} {
|
||||
delete_names.push(name.clone());
|
||||
}
|
||||
}
|
||||
seq.retain(|x| {
|
||||
if let Some(x_name) = if x.is_string() {
|
||||
Some(x)
|
||||
} else {
|
||||
x.get("name")
|
||||
} {
|
||||
!delete_names.contains(x_name)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
prepend.reverse();
|
||||
for item in prepend {
|
||||
seq.insert(0, item);
|
||||
}
|
||||
|
||||
for item in append {
|
||||
seq.push(item);
|
||||
}
|
||||
|
||||
let mut config = config.clone();
|
||||
config.insert(Value::from(name), Value::from(seq));
|
||||
config
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[allow(unused_imports)]
|
||||
use serde_yaml::Value;
|
||||
|
||||
#[test]
|
||||
fn test_delete_proxy_and_references() {
|
||||
let config_str = r#"
|
||||
proxies:
|
||||
- name: "proxy1"
|
||||
type: "ss"
|
||||
- name: "proxy2"
|
||||
type: "vmess"
|
||||
proxy-groups:
|
||||
- name: "group1"
|
||||
type: "select"
|
||||
proxies:
|
||||
- "proxy1"
|
||||
- "proxy2"
|
||||
- name: "group2"
|
||||
type: "select"
|
||||
proxies:
|
||||
- "proxy1"
|
||||
"#;
|
||||
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
|
||||
|
||||
let seq = SeqMap {
|
||||
prepend: Sequence::new(),
|
||||
append: Sequence::new(),
|
||||
delete: vec!["proxy1".to_string()],
|
||||
};
|
||||
|
||||
config = use_seq(seq, config, "proxies");
|
||||
|
||||
// Check if proxy1 is removed from proxies
|
||||
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
|
||||
assert_eq!(proxies.len(), 1);
|
||||
assert_eq!(
|
||||
proxies[0]
|
||||
.as_mapping()
|
||||
.unwrap()
|
||||
.get("name")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
"proxy2"
|
||||
);
|
||||
|
||||
// Check if proxy1 is removed from all groups
|
||||
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
|
||||
let group1_proxies = groups[0]
|
||||
.as_mapping()
|
||||
.unwrap()
|
||||
.get("proxies")
|
||||
.unwrap()
|
||||
.as_sequence()
|
||||
.unwrap();
|
||||
let group2_proxies = groups[1]
|
||||
.as_mapping()
|
||||
.unwrap()
|
||||
.get("proxies")
|
||||
.unwrap()
|
||||
.as_sequence()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(group1_proxies.len(), 1);
|
||||
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
|
||||
assert_eq!(group2_proxies.len(), 0);
|
||||
}
|
||||
}
|
||||
|
@ -24,57 +24,30 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
let dns_key = Value::from("dns");
|
||||
let dns_val = config.get(&dns_key);
|
||||
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
|
||||
if enable {
|
||||
// 读取DNS配置
|
||||
let dns_key = Value::from("dns");
|
||||
let dns_val = config.get(&dns_key);
|
||||
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
let ipv6_key = Value::from("ipv6");
|
||||
let ipv6_val = config
|
||||
.get(&ipv6_key)
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
// 检查现有的 enhanced-mode 设置
|
||||
let current_mode = dns_val
|
||||
.get(Value::from("enhanced-mode"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("fake-ip");
|
||||
|
||||
// 只有当 enhanced-mode 是 fake-ip 或未设置时才修改 DNS 配置
|
||||
if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) {
|
||||
revise!(dns_val, "enable", true);
|
||||
revise!(dns_val, "ipv6", ipv6_val);
|
||||
|
||||
if !dns_val.contains_key(Value::from("enhanced-mode")) {
|
||||
revise!(dns_val, "enhanced-mode", "fake-ip");
|
||||
}
|
||||
|
||||
if !dns_val.contains_key(Value::from("fake-ip-range")) {
|
||||
revise!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||
}
|
||||
revise!(dns_val, "enable", true);
|
||||
revise!(dns_val, "ipv6", true);
|
||||
revise!(dns_val, "enhanced-mode", "fake-ip");
|
||||
revise!(dns_val, "fake-ip-range", "10.96.0.0/16");
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
crate::utils::resolve::set_public_dns("10.96.0.2".to_string()).await;
|
||||
}
|
||||
|
||||
// 当TUN启用时,将修改后的DNS配置写回
|
||||
revise!(config, "dns", dns_val);
|
||||
} else {
|
||||
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
|
||||
revise!(dns_val, "enhanced-mode", "redir-host");
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
}
|
||||
|
||||
// 更新TUN配置
|
||||
revise!(tun_val, "enable", enable);
|
||||
revise!(config, "tun", tun_val);
|
||||
|
||||
revise!(config, "dns", dns_val);
|
||||
config
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
pub mod service;
|
@ -1 +0,0 @@
|
||||
|
483
src-tauri/src/feat.rs
Normal file
483
src-tauri/src/feat.rs
Normal file
@ -0,0 +1,483 @@
|
||||
//!
|
||||
//! feat mod 里的函数主要用于
|
||||
//! - hotkey 快捷键
|
||||
//! - timer 定时器
|
||||
//! - cmds 页面调用
|
||||
//!
|
||||
use crate::config::*;
|
||||
use crate::core::*;
|
||||
use crate::log_err;
|
||||
use crate::utils::dirs::app_home_dir;
|
||||
use crate::utils::resolve;
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||
|
||||
// 打开面板
|
||||
pub fn open_or_close_dashboard() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if let Ok(true) = window.is_focused() {
|
||||
let _ = window.hide();
|
||||
return;
|
||||
}
|
||||
}
|
||||
resolve::create_window();
|
||||
}
|
||||
|
||||
// 重启clash
|
||||
pub fn restart_clash_core() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match CoreManager::global().restart_core().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
handle::Handle::notice_message("set_config::ok", "ok");
|
||||
}
|
||||
Err(err) => {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target:"app", "{err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn restart_app() {
|
||||
tauri::async_runtime::spawn_blocking(|| {
|
||||
tauri::async_runtime::block_on(async {
|
||||
log_err!(CoreManager::global().stop_core().await);
|
||||
});
|
||||
resolve::resolve_reset();
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
let _ = app_handle.save_window_state(StateFlags::default());
|
||||
tauri::process::restart(&app_handle.env());
|
||||
});
|
||||
}
|
||||
|
||||
// 切换模式 rule/global/direct/script mode
|
||||
pub fn change_clash_mode(mode: String) {
|
||||
let mut mapping = Mapping::new();
|
||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
|
||||
match clash_api::patch_configs(&mapping).await {
|
||||
Ok(_) => {
|
||||
// 更新订阅
|
||||
Config::clash().data().patch_config(mapping);
|
||||
|
||||
if Config::clash().data().save_config().is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
log_err!(handle::Handle::update_systray_part());
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换系统代理
|
||||
pub fn toggle_system_proxy() {
|
||||
let enable = Config::verge().draft().enable_system_proxy;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match patch_verge(IVerge {
|
||||
enable_system_proxy: Some(!enable),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换tun模式
|
||||
pub fn toggle_tun_mode() {
|
||||
let enable = Config::verge().data().enable_tun_mode;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match patch_verge(IVerge {
|
||||
enable_tun_mode: Some(!enable),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn quit(code: Option<i32>) {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
handle::Handle::global().set_is_exiting();
|
||||
resolve::resolve_reset();
|
||||
log_err!(handle::Handle::global().get_window().unwrap().close());
|
||||
match app_handle.save_window_state(StateFlags::all()) {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "window state saved successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to save window state: {}", e);
|
||||
}
|
||||
};
|
||||
app_handle.exit(code.unwrap_or(0));
|
||||
}
|
||||
|
||||
/// 修改clash的订阅
|
||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
Config::clash().draft().patch_config(patch.clone());
|
||||
|
||||
let res = {
|
||||
// 激活订阅
|
||||
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
handle::Handle::refresh_clash();
|
||||
} else {
|
||||
if patch.get("mode").is_some() {
|
||||
log_err!(handle::Handle::update_systray_part());
|
||||
}
|
||||
|
||||
Config::runtime().latest().patch_config(patch);
|
||||
update_core_config(false).await?;
|
||||
}
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::clash().apply();
|
||||
Config::clash().data().save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::clash().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改verge的订阅
|
||||
/// 一般都是一个个的修改
|
||||
pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let pac = patch.proxy_auto_config;
|
||||
let pac_content = patch.pac_file_content;
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let language = patch.language;
|
||||
let mixed_port = patch.verge_mixed_port;
|
||||
#[cfg(target_os = "macos")]
|
||||
let tray_icon = patch.tray_icon;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let tray_icon: Option<String> = None;
|
||||
let common_tray_icon = patch.common_tray_icon;
|
||||
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
|
||||
let tun_tray_icon = patch.tun_tray_icon;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_enabled = patch.verge_redir_enabled;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_port = patch.verge_redir_port;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_enabled = patch.verge_tproxy_enabled;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_port = patch.verge_tproxy_port;
|
||||
let socks_enabled = patch.verge_socks_enabled;
|
||||
let socks_port = patch.verge_socks_port;
|
||||
let http_enabled = patch.verge_http_enabled;
|
||||
let http_port = patch.verge_port;
|
||||
|
||||
let res: std::result::Result<(), anyhow::Error> = {
|
||||
let mut should_restart_core = false;
|
||||
let mut should_update_clash_config = false;
|
||||
let mut should_update_launch = false;
|
||||
let mut should_update_sysproxy = false;
|
||||
let mut should_update_systray_part = false;
|
||||
|
||||
if tun_mode.is_some() {
|
||||
should_update_clash_config = true;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if redir_enabled.is_some() || redir_port.is_some() {
|
||||
should_restart_core = true;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
||||
should_restart_core = true;
|
||||
}
|
||||
if socks_enabled.is_some()
|
||||
|| http_enabled.is_some()
|
||||
|| socks_port.is_some()
|
||||
|| http_port.is_some()
|
||||
|| mixed_port.is_some()
|
||||
{
|
||||
should_restart_core = true;
|
||||
}
|
||||
if auto_launch.is_some() {
|
||||
should_update_launch = true;
|
||||
}
|
||||
if system_proxy.is_some()
|
||||
|| proxy_bypass.is_some()
|
||||
|| mixed_port.is_some()
|
||||
|| pac.is_some()
|
||||
|| pac_content.is_some()
|
||||
{
|
||||
should_update_sysproxy = true;
|
||||
}
|
||||
|
||||
if language.is_some()
|
||||
|| system_proxy.is_some()
|
||||
|| tun_mode.is_some()
|
||||
|| common_tray_icon.is_some()
|
||||
|| sysproxy_tray_icon.is_some()
|
||||
|| tun_tray_icon.is_some()
|
||||
|| tray_icon.is_some()
|
||||
{
|
||||
should_update_systray_part = true;
|
||||
}
|
||||
if should_restart_core {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if should_update_clash_config {
|
||||
update_core_config(false).await?;
|
||||
}
|
||||
if should_update_launch {
|
||||
sysopt::Sysopt::global().update_launch()?;
|
||||
}
|
||||
|
||||
if should_update_sysproxy {
|
||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||
}
|
||||
|
||||
if let Some(hotkeys) = patch.hotkeys {
|
||||
hotkey::Hotkey::global().update(hotkeys)?;
|
||||
}
|
||||
|
||||
if should_update_systray_part {
|
||||
handle::Handle::update_systray_part()?;
|
||||
}
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::verge().apply();
|
||||
Config::verge().data().save_file()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::verge().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新某个profile
|
||||
/// 如果更新当前订阅就激活订阅
|
||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
None // 直接更新
|
||||
} else if item.url.is_none() {
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||
}
|
||||
};
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
let merged_opt = PrfOption::merge(opt, option);
|
||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
Some(uid) == profiles.get_current()
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_update {
|
||||
update_core_config(true).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新订阅
|
||||
async fn update_core_config(notice: bool) -> Result<()> {
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
if notice {
|
||||
handle::Handle::notice_message("set_config::ok", "ok");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
if notice {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// copy env variable
|
||||
pub fn copy_clash_env() {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
||||
let http_proxy = format!("http://127.0.0.1:{}", port);
|
||||
let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
|
||||
|
||||
let sh =
|
||||
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
|
||||
let cmd: String = format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}");
|
||||
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
|
||||
|
||||
let cliboard = app_handle.clipboard();
|
||||
let env_type = { Config::verge().latest().env_type.clone() };
|
||||
let env_type = match env_type {
|
||||
Some(env_type) => env_type,
|
||||
None => {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let default = "bash";
|
||||
#[cfg(target_os = "windows")]
|
||||
let default = "powershell";
|
||||
|
||||
default.to_string()
|
||||
}
|
||||
};
|
||||
match env_type.as_str() {
|
||||
"bash" => cliboard.write_text(sh).unwrap_or_default(),
|
||||
"cmd" => cliboard.write_text(cmd).unwrap_or_default(),
|
||||
"powershell" => cliboard.write_text(ps).unwrap_or_default(),
|
||||
_ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn test_delay(url: String) -> Result<u32> {
|
||||
use tokio::time::{Duration, Instant};
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
if !tun_mode {
|
||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
let request = builder
|
||||
.timeout(Duration::from_millis(10000))
|
||||
.build()?
|
||||
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
|
||||
let start = Instant::now();
|
||||
|
||||
let response = request.send().await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
||||
if response.status().is_success() {
|
||||
Ok(start.elapsed().as_millis() as u32)
|
||||
} else {
|
||||
Ok(10000u32)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
||||
log::error!(target: "app", "Failed to create backup: {:#?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
if let Err(err) = backup::WebDavClient::global()
|
||||
.upload(temp_file_path.clone(), file_name)
|
||||
.await
|
||||
{
|
||||
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::remove_file(&temp_file_path) {
|
||||
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
|
||||
backup::WebDavClient::global().list().await.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
backup::WebDavClient::global()
|
||||
.delete(filename)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let backup_storage_path = app_home_dir().unwrap().join(&filename);
|
||||
backup::WebDavClient::global()
|
||||
.download(filename, backup_storage_path.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
// extract zip file
|
||||
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
|
||||
// 最后删除临时文件
|
||||
fs::remove_file(backup_storage_path)?;
|
||||
Ok(())
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::backup,
|
||||
logging_error,
|
||||
utils::{dirs::app_home_dir, logging::Type},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use std::fs;
|
||||
|
||||
/// Create a backup and upload to WebDAV
|
||||
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
||||
log::error!(target: "app", "Failed to create backup: {:#?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
if let Err(err) = backup::WebDavClient::global()
|
||||
.upload(temp_file_path.clone(), file_name)
|
||||
.await
|
||||
{
|
||||
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::remove_file(&temp_file_path) {
|
||||
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List WebDAV backups
|
||||
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
|
||||
backup::WebDavClient::global().list().await.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete WebDAV backup
|
||||
pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
backup::WebDavClient::global()
|
||||
.delete(filename)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
/// Restore WebDAV backup
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
let webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
|
||||
let backup_storage_path = app_home_dir().unwrap().join(&filename);
|
||||
backup::WebDavClient::global()
|
||||
.download(filename, backup_storage_path.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
// extract zip file
|
||||
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
logging_error!(
|
||||
Type::Backup,
|
||||
true,
|
||||
super::patch_verge(
|
||||
IVerge {
|
||||
webdav_url,
|
||||
webdav_username,
|
||||
webdav_password,
|
||||
..IVerge::default()
|
||||
},
|
||||
false
|
||||
)
|
||||
.await
|
||||
);
|
||||
// 最后删除临时文件
|
||||
fs::remove_file(backup_storage_path)?;
|
||||
Ok(())
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, tray, CoreManager},
|
||||
logging_error,
|
||||
module::mihomo::MihomoManager,
|
||||
utils::{logging::Type, resolve},
|
||||
};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use tauri::Manager;
|
||||
|
||||
/// Restart the Clash core
|
||||
pub fn restart_clash_core() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match CoreManager::global().restart_core().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
handle::Handle::notice_message("set_config::ok", "ok");
|
||||
}
|
||||
Err(err) => {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target:"app", "{err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Restart the application
|
||||
pub fn restart_app() {
|
||||
tauri::async_runtime::spawn_blocking(|| {
|
||||
tauri::async_runtime::block_on(async {
|
||||
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||
resolve::resolve_reset_async().await;
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
tauri::process::restart(&app_handle.env());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn after_change_clash_mode() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match MihomoManager::global().get_connections().await {
|
||||
Ok(connections) => {
|
||||
if let Some(connections_array) = connections["connections"].as_array() {
|
||||
for connection in connections_array {
|
||||
if let Some(id) = connection["id"].as_str() {
|
||||
let _ = MihomoManager::global().delete_connection(id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to get connections: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Change Clash mode (rule/global/direct/script)
|
||||
pub fn change_clash_mode(mode: String) {
|
||||
let mut mapping = Mapping::new();
|
||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||
// Convert YAML mapping to JSON Value
|
||||
let json_value = serde_json::json!({
|
||||
"mode": mode
|
||||
});
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
match MihomoManager::global().patch_configs(json_value).await {
|
||||
Ok(_) => {
|
||||
// 更新订阅
|
||||
Config::clash().data().patch_config(mapping);
|
||||
|
||||
if Config::clash().data().save_config().is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
||||
}
|
||||
|
||||
let is_auto_close_connection = Config::verge()
|
||||
.data()
|
||||
.auto_close_connection
|
||||
.unwrap_or(false);
|
||||
if is_auto_close_connection {
|
||||
after_change_clash_mode();
|
||||
}
|
||||
}
|
||||
Err(err) => println!("{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Test connection delay to a URL
|
||||
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
use tokio::time::{Duration, Instant};
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
if !tun_mode {
|
||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
let request = builder
|
||||
.timeout(Duration::from_millis(10000))
|
||||
.build()?
|
||||
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
|
||||
let start = Instant::now();
|
||||
|
||||
let response = request.send().await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
||||
if response.status().is_success() {
|
||||
Ok(start.elapsed().as_millis() as u32)
|
||||
} else {
|
||||
Ok(10000u32)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::{handle, hotkey, sysopt, tray, CoreManager},
|
||||
logging_error,
|
||||
module::lightweight,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use serde_yaml::Mapping;
|
||||
|
||||
/// Patch Clash configuration
|
||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
Config::clash().draft().patch_config(patch.clone());
|
||||
|
||||
let res = {
|
||||
// 激活订阅
|
||||
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
} else {
|
||||
if patch.get("mode").is_some() {
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
||||
}
|
||||
Config::runtime().latest().patch_config(patch);
|
||||
CoreManager::global().update_config().await?;
|
||||
}
|
||||
handle::Handle::refresh_clash();
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::clash().apply();
|
||||
Config::clash().data().save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::clash().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define update flags as bitflags for better performance
|
||||
#[derive(Clone, Copy)]
|
||||
enum UpdateFlags {
|
||||
None = 0,
|
||||
RestartCore = 1 << 0,
|
||||
ClashConfig = 1 << 1,
|
||||
VergeConfig = 1 << 2,
|
||||
Launch = 1 << 3,
|
||||
SysProxy = 1 << 4,
|
||||
SystrayIcon = 1 << 5,
|
||||
Hotkey = 1 << 6,
|
||||
SystrayMenu = 1 << 7,
|
||||
SystrayTooltip = 1 << 8,
|
||||
SystrayClickBehavior = 1 << 9,
|
||||
LighteWeight = 1 << 10,
|
||||
}
|
||||
|
||||
/// Patch Verge configuration
|
||||
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let pac = patch.proxy_auto_config;
|
||||
let pac_content = patch.pac_file_content;
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let language = patch.language;
|
||||
let mixed_port = patch.verge_mixed_port;
|
||||
#[cfg(target_os = "macos")]
|
||||
let tray_icon = patch.tray_icon;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let tray_icon: Option<String> = None;
|
||||
let common_tray_icon = patch.common_tray_icon;
|
||||
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
|
||||
let tun_tray_icon = patch.tun_tray_icon;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_enabled = patch.verge_redir_enabled;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_port = patch.verge_redir_port;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_enabled = patch.verge_tproxy_enabled;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_port = patch.verge_tproxy_port;
|
||||
let socks_enabled = patch.verge_socks_enabled;
|
||||
let socks_port = patch.verge_socks_port;
|
||||
let http_enabled = patch.verge_http_enabled;
|
||||
let http_port = patch.verge_port;
|
||||
let enable_tray_speed = patch.enable_tray_speed;
|
||||
let enable_tray_icon = patch.enable_tray_icon;
|
||||
let enable_global_hotkey = patch.enable_global_hotkey;
|
||||
let tray_event = patch.tray_event;
|
||||
let home_cards = patch.home_cards.clone();
|
||||
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
|
||||
let res: std::result::Result<(), anyhow::Error> = {
|
||||
// Initialize with no flags set
|
||||
let mut update_flags: i32 = UpdateFlags::None as i32;
|
||||
|
||||
if tun_mode.is_some() {
|
||||
update_flags |= UpdateFlags::ClashConfig as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
if enable_global_hotkey.is_some() || home_cards.is_some() {
|
||||
update_flags |= UpdateFlags::VergeConfig as i32;
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if redir_enabled.is_some() || redir_port.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
if socks_enabled.is_some()
|
||||
|| http_enabled.is_some()
|
||||
|| socks_port.is_some()
|
||||
|| http_port.is_some()
|
||||
|| mixed_port.is_some()
|
||||
{
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
if auto_launch.is_some() {
|
||||
update_flags |= UpdateFlags::Launch as i32;
|
||||
}
|
||||
|
||||
if system_proxy.is_some() {
|
||||
update_flags |= UpdateFlags::SysProxy as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
|
||||
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
|
||||
update_flags |= UpdateFlags::SysProxy as i32;
|
||||
}
|
||||
|
||||
if language.is_some() {
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
}
|
||||
if common_tray_icon.is_some()
|
||||
|| sysproxy_tray_icon.is_some()
|
||||
|| tun_tray_icon.is_some()
|
||||
|| tray_icon.is_some()
|
||||
|| enable_tray_speed.is_some()
|
||||
|| enable_tray_icon.is_some()
|
||||
{
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
|
||||
if patch.hotkeys.is_some() {
|
||||
update_flags |= UpdateFlags::Hotkey as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
}
|
||||
|
||||
if tray_event.is_some() {
|
||||
update_flags |= UpdateFlags::SystrayClickBehavior as i32;
|
||||
}
|
||||
|
||||
if enable_auto_light_weight.is_some() {
|
||||
update_flags |= UpdateFlags::LighteWeight as i32;
|
||||
}
|
||||
|
||||
// Process updates based on flags
|
||||
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
||||
CoreManager::global().update_config().await?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
|
||||
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
|
||||
handle::Handle::refresh_verge();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
|
||||
sysopt::Sysopt::global().update_launch()?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
|
||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
|
||||
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
|
||||
tray::Tray::global().update_menu()?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
|
||||
tray::Tray::global().update_icon(None)?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
|
||||
tray::Tray::global().update_tooltip()?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
|
||||
tray::Tray::global().update_click_behavior()?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
|
||||
if enable_auto_light_weight.unwrap() {
|
||||
lightweight::enable_auto_light_weight_mode();
|
||||
} else {
|
||||
lightweight::disable_auto_light_weight_mode();
|
||||
}
|
||||
}
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::verge().apply();
|
||||
if !not_save_file {
|
||||
Config::verge().data().save_file()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::verge().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
mod backup;
|
||||
mod clash;
|
||||
mod config;
|
||||
mod profile;
|
||||
mod proxy;
|
||||
mod window;
|
||||
|
||||
// Re-export all functions from modules
|
||||
pub use backup::*;
|
||||
pub use clash::*;
|
||||
pub use config::*;
|
||||
pub use profile::*;
|
||||
pub use proxy::*;
|
||||
pub use window::*;
|
@ -1,92 +0,0 @@
|
||||
use crate::{
|
||||
cmd,
|
||||
config::{Config, PrfItem, PrfOption},
|
||||
core::{handle, CoreManager, *},
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
/// Toggle proxy profile
|
||||
pub fn toggle_proxy_profile(profile_index: String) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
||||
Ok(_) => {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update a profile
|
||||
/// If updating current profile, activate it
|
||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||
println!("[订阅更新] 开始更新订阅 {}", uid);
|
||||
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
println!("[订阅更新] {} 缺少URL,无法更新", uid);
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
println!(
|
||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||
uid,
|
||||
item.url.clone().unwrap()
|
||||
);
|
||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||
}
|
||||
};
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
println!("[订阅更新] 开始下载新的订阅内容");
|
||||
let merged_opt = PrfOption::merge(opt, option);
|
||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||
|
||||
println!("[订阅更新] 更新订阅配置");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
is_current
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_update {
|
||||
println!("[订阅更新] 更新内核配置");
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
println!("[订阅更新] 更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[订阅更新] 更新失败: {}", err);
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 增强配置
|
||||
pub async fn enhance_profiles() -> Result<()> {
|
||||
crate::core::CoreManager::global()
|
||||
.update_config()
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle,
|
||||
};
|
||||
use std::env;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
/// Toggle system proxy on/off
|
||||
pub fn toggle_system_proxy() {
|
||||
let enable = Config::verge().draft().enable_system_proxy;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match super::patch_verge(
|
||||
IVerge {
|
||||
enable_system_proxy: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Toggle TUN mode on/off
|
||||
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
// tauri::async_runtime::spawn(async move {
|
||||
// logging!(
|
||||
// info,
|
||||
// Type::Service,
|
||||
// true,
|
||||
// "Toggle TUN mode need install service"
|
||||
// );
|
||||
// if is_service_available().await.is_err() {
|
||||
// logging_error!(Type::Service, true, install_service().await);
|
||||
// }
|
||||
// logging_error!(Type::Core, true, CoreManager::global().restart_core().await);
|
||||
// });
|
||||
|
||||
let enable = Config::verge().data().enable_tun_mode;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match super::patch_verge(
|
||||
IVerge {
|
||||
enable_tun_mode: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
not_save_file.unwrap_or(false),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Copy proxy environment variables to clipboard
|
||||
pub fn copy_clash_env() {
|
||||
// 从环境变量获取IP地址,默认127.0.0.1
|
||||
let clash_verge_rev_ip =
|
||||
env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
||||
let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
|
||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
|
||||
|
||||
let cliboard = app_handle.clipboard();
|
||||
let env_type = { Config::verge().latest().env_type.clone() };
|
||||
let env_type = match env_type {
|
||||
Some(env_type) => env_type,
|
||||
None => {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let default = "bash";
|
||||
#[cfg(target_os = "windows")]
|
||||
let default = "powershell";
|
||||
|
||||
default.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let export_text = match env_type.as_str() {
|
||||
"bash" => format!(
|
||||
"export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}"
|
||||
),
|
||||
"cmd" => format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}"),
|
||||
"powershell" => {
|
||||
format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"")
|
||||
}
|
||||
"nushell" => {
|
||||
format!("load-env {{ http_proxy: \"{http_proxy}\", https_proxy: \"{http_proxy}\" }}")
|
||||
}
|
||||
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
|
||||
_ => {
|
||||
log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if cliboard.write_text(export_text).is_err() {
|
||||
log::error!(target: "app", "Failed to write to clipboard");
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, sysopt, CoreManager},
|
||||
module::mihomo::MihomoManager,
|
||||
utils::resolve,
|
||||
};
|
||||
|
||||
/// Open or close the dashboard window
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard() {
|
||||
println!("Attempting to open/close dashboard");
|
||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
println!("Found existing window");
|
||||
log::info!(target: "app", "Found existing window");
|
||||
|
||||
// 如果窗口存在,则切换其显示状态
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
println!("Window visibility status: {}", visible);
|
||||
log::info!(target: "app", "Window visibility status: {}", visible);
|
||||
|
||||
if visible {
|
||||
println!("Attempting to hide window");
|
||||
log::info!(target: "app", "Attempting to hide window");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
println!("Attempting to show and focus window");
|
||||
log::info!(target: "app", "Attempting to show and focus window");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get window visibility: {:?}", e);
|
||||
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No existing window found, creating new window");
|
||||
log::info!(target: "app", "No existing window found, creating new window");
|
||||
resolve::create_window(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 优化的应用退出函数
|
||||
pub fn quit(code: Option<i32>) {
|
||||
log::debug!(target: "app", "启动退出流程");
|
||||
|
||||
// 获取应用句柄并设置退出标志
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
// 优先关闭窗口,提供立即反馈
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
}
|
||||
|
||||
// 在单独线程中处理资源清理,避免阻塞主线程
|
||||
std::thread::spawn(move || {
|
||||
// 使用tokio运行时执行异步清理任务
|
||||
tauri::async_runtime::block_on(async {
|
||||
// 使用超时机制处理清理操作
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
// 1. 直接关闭TUN模式 (优先处理,通常最容易卡住)
|
||||
if Config::verge().data().enable_tun_mode.unwrap_or(false) {
|
||||
let disable = serde_json::json!({
|
||||
"tun": {
|
||||
"enable": false
|
||||
}
|
||||
});
|
||||
|
||||
// 设置1秒超时
|
||||
let _ = timeout(
|
||||
Duration::from_secs(1),
|
||||
MihomoManager::global().patch_configs(disable),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// 2. 并行处理系统代理和核心进程清理
|
||||
let proxy_future = timeout(
|
||||
Duration::from_secs(1),
|
||||
sysopt::Sysopt::global().reset_sysproxy(),
|
||||
);
|
||||
|
||||
let core_future = timeout(Duration::from_secs(1), CoreManager::global().stop_core());
|
||||
|
||||
// 同时等待两个任务完成
|
||||
let _ = futures::join!(proxy_future, core_future);
|
||||
|
||||
// 3. 处理macOS特定清理
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await;
|
||||
}
|
||||
});
|
||||
|
||||
// 无论清理结果如何,确保应用退出
|
||||
app_handle.exit(code.unwrap_or(0));
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn hide() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,87 +1,15 @@
|
||||
mod cmd;
|
||||
mod cmds;
|
||||
mod config;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod error;
|
||||
mod feat;
|
||||
mod module;
|
||||
mod utils;
|
||||
use crate::{
|
||||
core::hotkey,
|
||||
utils::{resolve, resolve::resolve_scheme, server},
|
||||
};
|
||||
use config::Config;
|
||||
use std::sync::{Mutex, Once};
|
||||
use tauri::AppHandle;
|
||||
use crate::core::hotkey;
|
||||
use crate::utils::{resolve, resolve::resolve_scheme, server};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::Manager;
|
||||
use tauri::Listener;
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use utils::logging::Type;
|
||||
|
||||
/// A global singleton handle to the application.
|
||||
pub struct AppHandleManager {
|
||||
inner: Mutex<Option<AppHandle>>,
|
||||
init: Once,
|
||||
}
|
||||
|
||||
impl AppHandleManager {
|
||||
/// Get the global instance of the app handle manager.
|
||||
pub fn global() -> &'static Self {
|
||||
static INSTANCE: AppHandleManager = AppHandleManager {
|
||||
inner: Mutex::new(None),
|
||||
init: Once::new(),
|
||||
};
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
/// Initialize the app handle manager with an app handle.
|
||||
pub fn init(&self, handle: AppHandle) {
|
||||
self.init.call_once(|| {
|
||||
let mut app_handle = self.inner.lock().unwrap();
|
||||
*app_handle = Some(handle);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the app handle if it has been initialized.
|
||||
pub fn get(&self) -> Option<AppHandle> {
|
||||
self.inner.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Get the app handle, panics if it hasn't been initialized.
|
||||
pub fn get_handle(&self) -> AppHandle {
|
||||
self.get().expect("AppHandle not initialized")
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_regular(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_accessory(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_prohibited(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
pub fn run() {
|
||||
// 单例检测
|
||||
let app_exists: bool = tauri::async_runtime::block_on(async move {
|
||||
@ -101,6 +29,7 @@ pub fn run() {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let devtools = tauri_plugin_devtools::init();
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
@ -112,114 +41,96 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.setup(|app| {
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
logging_error!(Type::System, true, app.deep_link().register_all());
|
||||
log_err!(app.deep_link().register_all());
|
||||
}
|
||||
|
||||
app.deep_link().on_open_url(|event| {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Some(url) = event.urls().first() {
|
||||
logging_error!(Type::Setup, true, resolve_scheme(url.to_string()).await);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
app.listen("deep-link://new-url", |event| {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let payload = event.payload();
|
||||
log_err!(resolve_scheme(payload.to_owned()).await);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
tauri::async_runtime::block_on(async move {
|
||||
resolve::resolve_setup(app).await;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let argvs: Vec<String> = std::env::args().collect();
|
||||
if argvs.len() > 1 {
|
||||
log_err!(resolve_scheme(argvs[1].to_owned()).await);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
cmd::get_sys_proxy,
|
||||
cmd::get_auto_proxy,
|
||||
cmd::open_app_dir,
|
||||
cmd::open_logs_dir,
|
||||
cmd::open_web_url,
|
||||
cmd::open_core_dir,
|
||||
cmd::get_portable_flag,
|
||||
cmd::get_network_interfaces,
|
||||
cmd::restart_core,
|
||||
cmd::restart_app,
|
||||
// 添加新的命令
|
||||
cmd::get_running_mode,
|
||||
cmd::get_app_uptime,
|
||||
cmd::get_auto_launch_status,
|
||||
cmd::is_admin,
|
||||
// service 管理
|
||||
cmd::install_service,
|
||||
cmd::uninstall_service,
|
||||
cmd::reinstall_service,
|
||||
cmd::repair_service,
|
||||
cmds::get_sys_proxy,
|
||||
cmds::get_auto_proxy,
|
||||
cmds::open_app_dir,
|
||||
cmds::open_logs_dir,
|
||||
cmds::open_web_url,
|
||||
cmds::open_core_dir,
|
||||
cmds::get_portable_flag,
|
||||
cmds::get_network_interfaces,
|
||||
// cmds::kill_sidecar,
|
||||
cmds::restart_core,
|
||||
cmds::restart_app,
|
||||
// clash
|
||||
cmd::get_clash_info,
|
||||
cmd::patch_clash_config,
|
||||
cmd::patch_clash_mode,
|
||||
cmd::change_clash_core,
|
||||
cmd::get_runtime_config,
|
||||
cmd::get_runtime_yaml,
|
||||
cmd::get_runtime_exists,
|
||||
cmd::get_runtime_logs,
|
||||
cmd::invoke_uwp_tool,
|
||||
cmd::copy_clash_env,
|
||||
cmd::get_proxies,
|
||||
cmd::get_providers_proxies,
|
||||
cmd::save_dns_config,
|
||||
cmd::apply_dns_config,
|
||||
cmd::check_dns_config_exists,
|
||||
cmd::get_dns_config_content,
|
||||
cmds::get_clash_info,
|
||||
cmds::patch_clash_config,
|
||||
cmds::change_clash_core,
|
||||
cmds::get_runtime_config,
|
||||
cmds::get_runtime_yaml,
|
||||
cmds::get_runtime_exists,
|
||||
cmds::get_runtime_logs,
|
||||
cmds::uwp::invoke_uwp_tool,
|
||||
cmds::copy_clash_env,
|
||||
// verge
|
||||
cmd::get_verge_config,
|
||||
cmd::patch_verge_config,
|
||||
cmd::test_delay,
|
||||
cmd::get_app_dir,
|
||||
cmd::copy_icon_file,
|
||||
cmd::download_icon_cache,
|
||||
cmd::open_devtools,
|
||||
cmd::exit_app,
|
||||
cmd::get_network_interfaces_info,
|
||||
cmds::get_verge_config,
|
||||
cmds::patch_verge_config,
|
||||
cmds::test_delay,
|
||||
cmds::get_app_dir,
|
||||
cmds::copy_icon_file,
|
||||
cmds::download_icon_cache,
|
||||
cmds::open_devtools,
|
||||
cmds::exit_app,
|
||||
cmds::get_network_interfaces_info,
|
||||
// cmds::update_hotkeys,
|
||||
// profile
|
||||
cmd::get_profiles,
|
||||
cmd::enhance_profiles,
|
||||
cmd::patch_profiles_config,
|
||||
cmd::view_profile,
|
||||
cmd::patch_profile,
|
||||
cmd::create_profile,
|
||||
cmd::import_profile,
|
||||
cmd::reorder_profile,
|
||||
cmd::update_profile,
|
||||
cmd::delete_profile,
|
||||
cmd::read_profile_file,
|
||||
cmd::save_profile_file,
|
||||
// script validation
|
||||
cmd::script_validate_notice,
|
||||
cmd::validate_script_file,
|
||||
cmds::get_profiles,
|
||||
cmds::enhance_profiles,
|
||||
cmds::patch_profiles_config,
|
||||
cmds::view_profile,
|
||||
cmds::patch_profile,
|
||||
cmds::create_profile,
|
||||
cmds::import_profile,
|
||||
cmds::reorder_profile,
|
||||
cmds::update_profile,
|
||||
cmds::delete_profile,
|
||||
cmds::read_profile_file,
|
||||
cmds::save_profile_file,
|
||||
// clash api
|
||||
cmd::clash_api_get_proxy_delay,
|
||||
cmds::clash_api_get_proxy_delay,
|
||||
// backup
|
||||
cmd::create_webdav_backup,
|
||||
cmd::save_webdav_config,
|
||||
cmd::list_webdav_backup,
|
||||
cmd::delete_webdav_backup,
|
||||
cmd::restore_webdav_backup,
|
||||
// export diagnostic info for issue reporting
|
||||
cmd::export_diagnostic_info,
|
||||
// get system info for display
|
||||
cmd::get_system_info,
|
||||
// media unlock checker
|
||||
cmd::get_unlock_items,
|
||||
cmd::check_media_unlock,
|
||||
// light-weight model
|
||||
cmd::entry_lightweight_mode,
|
||||
cmds::create_webdav_backup,
|
||||
cmds::save_webdav_config,
|
||||
cmds::list_webdav_backup,
|
||||
cmds::delete_webdav_backup,
|
||||
cmds::restore_webdav_backup,
|
||||
]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@ -227,50 +138,21 @@ pub fn run() {
|
||||
builder = builder.plugin(devtools);
|
||||
}
|
||||
|
||||
// Macos Application Menu
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Temporary Achived due to cannot CMD+C/V/A
|
||||
}
|
||||
|
||||
let app = builder
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(window) = AppHandleManager::global()
|
||||
.get_handle()
|
||||
.get_webview_window("main")
|
||||
{
|
||||
let _ = window.set_title("Clash Verge");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::RunEvent::Reopen {
|
||||
has_visible_windows,
|
||||
..
|
||||
} => {
|
||||
if !has_visible_windows {
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
}
|
||||
app.run(|_, e| match e {
|
||||
tauri::RunEvent::ExitRequested { api, code, .. } => {
|
||||
if code.is_none() {
|
||||
api.prevent_exit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
@ -282,90 +164,33 @@ pub fn run() {
|
||||
tauri::WindowEvent::Focused(true) => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().register("CMD+Q", "quit")
|
||||
);
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().register("CMD+W", "hide")
|
||||
);
|
||||
log_err!(hotkey::Hotkey::global().register("CMD+Q", "quit"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().register("Control+Q", "quit")
|
||||
);
|
||||
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit"));
|
||||
};
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().init())
|
||||
}
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Focused(false) => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("CMD+Q")
|
||||
);
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("CMD+W")
|
||||
);
|
||||
log_err!(hotkey::Hotkey::global().unregister("CMD+Q"));
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("Control+Q")
|
||||
);
|
||||
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
||||
};
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().reset())
|
||||
}
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Destroyed => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("CMD+Q")
|
||||
);
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("CMD+W")
|
||||
);
|
||||
log_err!(hotkey::Hotkey::global().unregister("CMD+Q"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("Control+Q")
|
||||
);
|
||||
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
|
@ -1,142 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::TaskBuilder;
|
||||
use tauri::{Listener, Manager};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, timer::Timer},
|
||||
log_err, logging,
|
||||
utils::logging::Type,
|
||||
AppHandleManager,
|
||||
};
|
||||
|
||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||
|
||||
pub fn enable_auto_light_weight_mode() {
|
||||
Timer::global().init().unwrap();
|
||||
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
|
||||
setup_window_close_listener();
|
||||
setup_webview_focus_listener();
|
||||
}
|
||||
|
||||
pub fn disable_auto_light_weight_mode() {
|
||||
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
|
||||
let _ = cancel_light_weight_timer();
|
||||
cancel_window_close_listener();
|
||||
}
|
||||
|
||||
pub fn entry_lightweight_mode() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
let _ = window.hide();
|
||||
}
|
||||
if let Some(webview) = window.get_webview_window("main") {
|
||||
let _ = webview.destroy();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
}
|
||||
let _ = cancel_light_weight_timer();
|
||||
}
|
||||
|
||||
fn setup_window_close_listener() -> u32 {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let handler = window.listen("tauri://close-requested", move |_event| {
|
||||
let _ = setup_light_weight_timer();
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"监听到关闭请求,开始轻量模式计时"
|
||||
);
|
||||
});
|
||||
return handler;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn setup_webview_focus_listener() -> u32 {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let handler = window.listen("tauri://focus", move |_event| {
|
||||
log_err!(cancel_light_weight_timer());
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"监听到窗口获得焦点,取消轻量模式计时"
|
||||
);
|
||||
});
|
||||
return handler;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn cancel_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
window.unlisten(setup_window_close_listener());
|
||||
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_light_weight_timer() -> Result<()> {
|
||||
Timer::global().init()?;
|
||||
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
let mut timer_count = Timer::global().timer_count.lock();
|
||||
|
||||
let task_id = *timer_count;
|
||||
*timer_count += 1;
|
||||
|
||||
let once_by_minutes = Config::verge()
|
||||
.latest()
|
||||
.auto_light_weight_minutes
|
||||
.unwrap_or(10);
|
||||
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(task_id)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_once_by_minutes(once_by_minutes)
|
||||
.spawn_async_routine(move || async move {
|
||||
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
|
||||
entry_lightweight_mode();
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
delay_timer
|
||||
.add_task(task)
|
||||
.context("failed to add timer task")?;
|
||||
|
||||
let timer_task = crate::core::timer::TimerTask {
|
||||
task_id,
|
||||
interval_minutes: once_by_minutes,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
true,
|
||||
"计时器已设置,{} 分钟后将自动进入轻量模式",
|
||||
once_by_minutes
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_light_weight_timer() -> Result<()> {
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
|
||||
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) {
|
||||
delay_timer
|
||||
.remove_task(task.task_id)
|
||||
.context("failed to remove timer task")?;
|
||||
logging!(info, Type::Timer, true, "计时器已取消");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use mihomo_api;
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use std::sync::Mutex;
|
||||
use tauri::http::{HeaderMap, HeaderValue};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tokio_tungstenite::tungstenite::http;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Rate {
|
||||
pub up: u64,
|
||||
pub down: u64,
|
||||
}
|
||||
|
||||
pub struct MihomoManager {
|
||||
mihomo: Mutex<OnceCell<mihomo_api::MihomoManager>>,
|
||||
}
|
||||
|
||||
impl MihomoManager {
|
||||
fn __global() -> &'static MihomoManager {
|
||||
static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager {
|
||||
mihomo: Mutex::new(OnceCell::new()),
|
||||
});
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
pub fn global() -> mihomo_api::MihomoManager {
|
||||
let instance = MihomoManager::__global();
|
||||
let (current_server, headers) = MihomoManager::get_clash_client_info().unwrap();
|
||||
|
||||
let lock = instance.mihomo.lock().unwrap();
|
||||
if let Some(mihomo) = lock.get() {
|
||||
if mihomo.get_mihomo_server() == current_server {
|
||||
return mihomo.clone();
|
||||
}
|
||||
}
|
||||
|
||||
lock.set(mihomo_api::MihomoManager::new(current_server, headers))
|
||||
.ok();
|
||||
lock.get().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl MihomoManager {
|
||||
pub fn get_clash_client_info() -> Option<(String, HeaderMap)> {
|
||||
let client = { Config::clash().data().get_client_info() };
|
||||
let server = format!("http://{}", client.server);
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||
if let Some(secret) = client.secret {
|
||||
let secret = format!("Bearer {}", secret).parse().unwrap();
|
||||
headers.insert("Authorization", secret);
|
||||
}
|
||||
|
||||
Some((server, headers))
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_traffic_ws_url() -> (String, HeaderValue) {
|
||||
let (url, headers) = MihomoManager::get_clash_client_info().unwrap();
|
||||
let ws_url = url.replace("http://", "ws://") + "/traffic";
|
||||
let auth = headers
|
||||
.get("Authorization")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let token = http::header::HeaderValue::from_str(&auth).unwrap();
|
||||
(ws_url, token)
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
pub mod lightweight;
|
||||
pub mod mihomo;
|
||||
pub mod sysinfo;
|
@ -1,64 +0,0 @@
|
||||
use crate::{
|
||||
cmd::system,
|
||||
core::{handle, CoreManager},
|
||||
};
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use sysinfo::System;
|
||||
|
||||
pub struct PlatformSpecification {
|
||||
system_name: String,
|
||||
system_version: String,
|
||||
system_kernel_version: String,
|
||||
system_arch: String,
|
||||
verge_version: String,
|
||||
running_mode: String,
|
||||
is_admin: bool,
|
||||
}
|
||||
|
||||
impl Debug for PlatformSpecification {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}",
|
||||
self.system_name, self.system_version, self.system_kernel_version, self.system_arch, self.verge_version, self.running_mode, self.is_admin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformSpecification {
|
||||
pub fn new() -> Self {
|
||||
let system_name = System::name().unwrap_or("Null".into());
|
||||
let system_version = System::long_os_version().unwrap_or("Null".into());
|
||||
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
|
||||
let system_arch = System::cpu_arch();
|
||||
|
||||
let handler = handle::Handle::global().app_handle().unwrap();
|
||||
let config = handler.config();
|
||||
let verge_version = config.version.clone().unwrap_or("Null".into());
|
||||
|
||||
// 使用默认值避免在同步上下文中执行异步操作
|
||||
let running_mode = "NotRunning".to_string();
|
||||
|
||||
let is_admin = system::is_admin().unwrap_or_default();
|
||||
|
||||
Self {
|
||||
system_name,
|
||||
system_version,
|
||||
system_kernel_version,
|
||||
system_arch,
|
||||
verge_version,
|
||||
running_mode,
|
||||
is_admin,
|
||||
}
|
||||
}
|
||||
|
||||
// 异步方法来获取完整的系统信息
|
||||
pub async fn new_async() -> Self {
|
||||
let mut info = Self::new();
|
||||
|
||||
let running_mode = CoreManager::global().get_running_mode().await;
|
||||
info.running_mode = running_mode.to_string();
|
||||
|
||||
info
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
use crate::core::handle;
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::{fs, path::PathBuf};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::Manager;
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
@ -77,36 +78,6 @@ pub fn app_profiles_dir() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join("profiles"))
|
||||
}
|
||||
|
||||
/// icons dir
|
||||
pub fn app_icons_dir() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join("icons"))
|
||||
}
|
||||
|
||||
pub fn find_target_icons(target: &str) -> Result<Option<String>> {
|
||||
let icons_dir = app_icons_dir()?;
|
||||
let mut matching_files = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(icons_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if file_name.starts_with(target)
|
||||
&& (file_name.ends_with(".ico") || file_name.ends_with(".png"))
|
||||
{
|
||||
matching_files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matching_files.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let first = path_to_str(matching_files.first().unwrap())?;
|
||||
Ok(Some(first.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// logs dir
|
||||
pub fn app_logs_dir() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join("logs"))
|
||||
@ -166,7 +137,7 @@ pub fn get_encryption_key() -> Result<Vec<u8>> {
|
||||
} else {
|
||||
// Generate and save new key
|
||||
let mut key = vec![0u8; 32];
|
||||
getrandom::fill(&mut key)?;
|
||||
getrandom::getrandom(&mut key)?;
|
||||
|
||||
// Ensure directory exists
|
||||
if let Some(parent) = key_path.parent() {
|
||||
|
@ -1,9 +1,10 @@
|
||||
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
|
||||
use crate::enhance::seq::SeqMap;
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use nanoid::nanoid;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::{fs, path::PathBuf, str::FromStr};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
/// read data from yaml as struct T
|
||||
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
@ -22,41 +23,19 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// read mapping from yaml
|
||||
/// read mapping from yaml fix #165
|
||||
pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||
if !path.exists() {
|
||||
bail!("file not found \"{}\"", path.display());
|
||||
}
|
||||
let mut val: Value = read_yaml(path)?;
|
||||
val.apply_merge()
|
||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
|
||||
let yaml_str = fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
|
||||
// YAML语法检查
|
||||
match serde_yaml::from_str::<serde_yaml::Value>(&yaml_str) {
|
||||
Ok(mut val) => {
|
||||
val.apply_merge()
|
||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
|
||||
Ok(val
|
||||
.as_mapping()
|
||||
.ok_or(anyhow!(
|
||||
"failed to transform to yaml mapping \"{}\"",
|
||||
path.display()
|
||||
))?
|
||||
.to_owned())
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("YAML syntax error in {}: {}", path.display(), err);
|
||||
logging!(error, Type::Config, true, "{}", error_msg);
|
||||
|
||||
crate::core::handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
&error_msg,
|
||||
);
|
||||
|
||||
bail!("YAML syntax error: {}", err)
|
||||
}
|
||||
}
|
||||
Ok(val
|
||||
.as_mapping()
|
||||
.ok_or(anyhow!(
|
||||
"failed to transform to yaml mapping \"{}\"",
|
||||
path.display()
|
||||
))?
|
||||
.to_owned())
|
||||
}
|
||||
|
||||
/// read mapping from yaml fix #165
|
||||
@ -120,24 +99,12 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
/// open file
|
||||
pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||
open::that_detached(path.as_os_str())?;
|
||||
/// use vscode by default
|
||||
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||
app.shell().open(path.to_string_lossy(), None).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn is_monochrome_image_from_bytes(data: &[u8]) -> anyhow::Result<bool> {
|
||||
let img = image::load_from_memory(data)?;
|
||||
let rgb_img = img.to_rgb8();
|
||||
|
||||
for pixel in rgb_img.pixels() {
|
||||
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn linux_elevator() -> String {
|
||||
use std::process::Command;
|
||||
@ -158,6 +125,52 @@ pub fn linux_elevator() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($result: expr) => {
|
||||
log::error!(target: "app", "{}", $result);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_err {
|
||||
($result: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
};
|
||||
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(_) = $result {
|
||||
log::error!(target: "app", "{}", $err_str);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace_err {
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// wrap the anyhow error
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
macro_rules! wrap_err {
|
||||
($stat: expr) => {
|
||||
match $stat {
|
||||
Ok(a) => Ok(a),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{}", err.to_string());
|
||||
Err(format!("{}", err.to_string()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// return the string literal error
|
||||
#[macro_export]
|
||||
macro_rules! ret_err {
|
||||
@ -177,38 +190,22 @@ macro_rules! t {
|
||||
};
|
||||
}
|
||||
|
||||
/// 将字节数转换为可读的流量字符串
|
||||
/// 支持 B/s、KB/s、MB/s、GB/s 的自动转换
|
||||
///
|
||||
/// # Examples
|
||||
/// ```not_run
|
||||
/// format_bytes_speed(1000) // returns "1000B/s"
|
||||
/// format_bytes_speed(1024) // returns "1.0KB/s"
|
||||
/// format_bytes_speed(1024 * 1024) // returns "1.0MB/s"
|
||||
/// ```
|
||||
/// ```
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn format_bytes_speed(speed: u64) -> String {
|
||||
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
||||
let mut size = speed as f64;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size >= 1000.0 && unit_index < UNITS.len() - 1 {
|
||||
size /= 1024.0;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
format!("{:.1}{}/s", size, UNITS[unit_index])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_format_bytes_speed() {
|
||||
assert_eq!(format_bytes_speed(0), "0B/s");
|
||||
assert_eq!(format_bytes_speed(1023), "1023B/s");
|
||||
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 500), "500.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024 * 2), "2.0MB/s");
|
||||
fn test_parse_value() {
|
||||
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||
let test_2 = "attachment; filename=Clash.yaml";
|
||||
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload").unwrap(), 111);
|
||||
assert_eq!(parse_str::<usize>(test_1, "download").unwrap(), 2222);
|
||||
assert_eq!(parse_str::<usize>(test_1, "total").unwrap(), 3333);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire").unwrap(), 444);
|
||||
assert_eq!(
|
||||
parse_str::<String>(test_2, "filename").unwrap(),
|
||||
format!("Clash.yaml")
|
||||
);
|
||||
|
||||
assert_eq!(parse_str::<usize>(test_1, "aaa"), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload1"), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire1"), None);
|
||||
assert_eq!(parse_str::<usize>(test_2, "attachment"), None);
|
||||
}
|
||||
|
@ -1,87 +0,0 @@
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, fs, path::PathBuf};
|
||||
use sys_locale;
|
||||
|
||||
const DEFAULT_LANGUAGE: &str = "zh";
|
||||
|
||||
fn get_locales_dir() -> Option<PathBuf> {
|
||||
dirs::app_resources_dir()
|
||||
.map(|resource_path| resource_path.join("locales"))
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn get_supported_languages() -> Vec<String> {
|
||||
let mut languages = Vec::new();
|
||||
|
||||
if let Some(locales_dir) = get_locales_dir() {
|
||||
if let Ok(entries) = fs::read_dir(locales_dir) {
|
||||
for entry in entries.flatten() {
|
||||
if let Some(file_name) = entry.file_name().to_str() {
|
||||
if let Some(lang) = file_name.strip_suffix(".json") {
|
||||
languages.push(lang.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if languages.is_empty() {
|
||||
languages.push(DEFAULT_LANGUAGE.to_string());
|
||||
}
|
||||
languages
|
||||
}
|
||||
|
||||
static TRANSLATIONS: Lazy<HashMap<String, Value>> = Lazy::new(|| {
|
||||
let mut translations = HashMap::new();
|
||||
|
||||
if let Some(locales_dir) = get_locales_dir() {
|
||||
for lang in get_supported_languages() {
|
||||
let file_path = locales_dir.join(format!("{}.json", lang));
|
||||
if let Ok(content) = fs::read_to_string(file_path) {
|
||||
if let Ok(json) = serde_json::from_str(&content) {
|
||||
translations.insert(lang.to_string(), json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
translations
|
||||
});
|
||||
|
||||
fn get_system_language() -> String {
|
||||
sys_locale::get_locale()
|
||||
.map(|locale| locale.to_lowercase())
|
||||
.and_then(|locale| locale.split(['_', '-']).next().map(String::from))
|
||||
.filter(|lang| get_supported_languages().contains(lang))
|
||||
.unwrap_or_else(|| DEFAULT_LANGUAGE.to_string())
|
||||
}
|
||||
|
||||
pub fn t(key: &str) -> String {
|
||||
let current_lang = Config::verge()
|
||||
.latest()
|
||||
.language
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.unwrap_or_else(get_system_language);
|
||||
|
||||
if let Some(text) = TRANSLATIONS
|
||||
.get(¤t_lang)
|
||||
.and_then(|trans| trans.get(key))
|
||||
.and_then(|val| val.as_str())
|
||||
{
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
if current_lang != DEFAULT_LANGUAGE {
|
||||
if let Some(text) = TRANSLATIONS
|
||||
.get(DEFAULT_LANGUAGE)
|
||||
.and_then(|trans| trans.get(key))
|
||||
.and_then(|val| val.as_str())
|
||||
{
|
||||
return text.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
key.to_string()
|
||||
}
|
@ -1,21 +1,16 @@
|
||||
use crate::{
|
||||
config::*,
|
||||
core::handle,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use crate::config::*;
|
||||
use crate::core::handle;
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use chrono::{Local, TimeZone};
|
||||
use log::LevelFilter;
|
||||
use log4rs::{
|
||||
append::{console::ConsoleAppender, file::FileAppender},
|
||||
config::{Appender, Logger, Root},
|
||||
encode::pattern::PatternEncoder,
|
||||
};
|
||||
use std::{
|
||||
fs::{self, DirEntry},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use std::fs::{self, DirEntry};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
/// initialize this instance's log file
|
||||
@ -138,113 +133,6 @@ pub fn delete_log() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 初始化DNS配置文件
|
||||
fn init_dns_config() -> Result<()> {
|
||||
use serde_yaml::Value;
|
||||
|
||||
// 获取默认DNS配置
|
||||
let default_dns_config = serde_yaml::Mapping::from_iter([
|
||||
("enable".into(), Value::Bool(true)),
|
||||
("listen".into(), Value::String(":53".into())),
|
||||
("enhanced-mode".into(), Value::String("fake-ip".into())),
|
||||
(
|
||||
"fake-ip-range".into(),
|
||||
Value::String("198.18.0.1/16".into()),
|
||||
),
|
||||
(
|
||||
"fake-ip-filter-mode".into(),
|
||||
Value::String("blacklist".into()),
|
||||
),
|
||||
("prefer-h3".into(), Value::Bool(false)),
|
||||
("respect-rules".into(), Value::Bool(false)),
|
||||
("use-hosts".into(), Value::Bool(false)),
|
||||
("use-system-hosts".into(), Value::Bool(false)),
|
||||
(
|
||||
"fake-ip-filter".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("*.lan".into()),
|
||||
Value::String("*.local".into()),
|
||||
Value::String("*.arpa".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("ntp.*.com".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("+.market.xiaomi.com".into()),
|
||||
Value::String("localhost.ptlogin2.qq.com".into()),
|
||||
Value::String("*.msftncsi.com".into()),
|
||||
Value::String("www.msftconnecttest.com".into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"default-nameserver".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("system".into()),
|
||||
Value::String("223.6.6.6".into()),
|
||||
Value::String("8.8.8.8".into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"nameserver".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("8.8.8.8".into()),
|
||||
Value::String("https://doh.pub/dns-query".into()),
|
||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||
]),
|
||||
),
|
||||
("fallback".into(), Value::Sequence(vec![])),
|
||||
(
|
||||
"nameserver-policy".into(),
|
||||
Value::Mapping(serde_yaml::Mapping::new()),
|
||||
),
|
||||
(
|
||||
"proxy-server-nameserver".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("https://doh.pub/dns-query".into()),
|
||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||
Value::String("tls://223.5.5.5".into()),
|
||||
]),
|
||||
),
|
||||
("direct-nameserver".into(), Value::Sequence(vec![])),
|
||||
("direct-nameserver-follow-policy".into(), Value::Bool(false)),
|
||||
(
|
||||
"fallback-filter".into(),
|
||||
Value::Mapping(serde_yaml::Mapping::from_iter([
|
||||
("geoip".into(), Value::Bool(true)),
|
||||
("geoip-code".into(), Value::String("CN".into())),
|
||||
(
|
||||
"ipcidr".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("240.0.0.0/4".into()),
|
||||
Value::String("0.0.0.0/32".into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"domain".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("+.google.com".into()),
|
||||
Value::String("+.facebook.com".into()),
|
||||
Value::String("+.youtube.com".into()),
|
||||
]),
|
||||
),
|
||||
])),
|
||||
),
|
||||
]);
|
||||
|
||||
// 检查DNS配置文件是否存在
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let dns_path = app_dir.join("dns_config.yaml");
|
||||
|
||||
if !dns_path.exists() {
|
||||
log::info!(target: "app", "Creating default DNS config file");
|
||||
help::save_yaml(
|
||||
&dns_path,
|
||||
&default_dns_config,
|
||||
Some("# Clash Verge DNS Config"),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize all the config files
|
||||
/// before tauri setup
|
||||
pub fn init_config() -> Result<()> {
|
||||
@ -285,9 +173,6 @@ pub fn init_config() -> Result<()> {
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
// 初始化DNS配置文件
|
||||
let _ = init_dns_config();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -295,22 +180,32 @@ pub fn init_config() -> Result<()> {
|
||||
/// after tauri setup
|
||||
pub fn init_resources() -> Result<()> {
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let test_dir = app_dir.join("test");
|
||||
let res_dir = dirs::app_resources_dir()?;
|
||||
|
||||
if !app_dir.exists() {
|
||||
let _ = fs::create_dir_all(&app_dir);
|
||||
}
|
||||
if !test_dir.exists() {
|
||||
let _ = fs::create_dir_all(&test_dir);
|
||||
}
|
||||
if !res_dir.exists() {
|
||||
let _ = fs::create_dir_all(&res_dir);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
||||
#[cfg(target_os = "macos")]
|
||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
||||
#[cfg(target_os = "linux")]
|
||||
let file_list: [&str; 0] = [];
|
||||
|
||||
// copy the resource file
|
||||
// if the source file is newer than the destination file, copy it over
|
||||
for file in file_list.iter() {
|
||||
let src_path = res_dir.join(file);
|
||||
let dest_path = app_dir.join(file);
|
||||
let test_dest_path = test_dir.join(file);
|
||||
log::debug!(target: "app", "src_path: {src_path:?}, dest_path: {dest_path:?}");
|
||||
|
||||
let handle_copy = |dest: &PathBuf| {
|
||||
@ -322,6 +217,9 @@ pub fn init_resources() -> Result<()> {
|
||||
};
|
||||
};
|
||||
|
||||
if src_path.exists() && !test_dest_path.exists() {
|
||||
handle_copy(&test_dest_path);
|
||||
}
|
||||
if src_path.exists() && !dest_path.exists() {
|
||||
handle_copy(&dest_path);
|
||||
continue;
|
||||
@ -352,7 +250,8 @@ pub fn init_resources() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
use tauri::utils::platform::current_exe;
|
||||
use winreg::{enums::*, RegKey};
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
|
@ -1,139 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Cmd,
|
||||
Core,
|
||||
Config,
|
||||
Setup,
|
||||
System,
|
||||
Service,
|
||||
Hotkey,
|
||||
Window,
|
||||
Tray,
|
||||
Timer,
|
||||
Frontend,
|
||||
Backup,
|
||||
Lightweight,
|
||||
}
|
||||
|
||||
impl fmt::Display for Type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Type::Cmd => write!(f, "[Cmd]"),
|
||||
Type::Core => write!(f, "[Core]"),
|
||||
Type::Config => write!(f, "[Config]"),
|
||||
Type::Setup => write!(f, "[Setup]"),
|
||||
Type::System => write!(f, "[System]"),
|
||||
Type::Service => write!(f, "[Service]"),
|
||||
Type::Hotkey => write!(f, "[Hotkey]"),
|
||||
Type::Window => write!(f, "[Window]"),
|
||||
Type::Tray => write!(f, "[Tray]"),
|
||||
Type::Timer => write!(f, "[Timer]"),
|
||||
Type::Frontend => write!(f, "[Frontend]"),
|
||||
Type::Backup => write!(f, "[Backup]"),
|
||||
Type::Lightweight => write!(f, "[Lightweight]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($result: expr) => {
|
||||
log::error!(target: "app", "{}", $result);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_err {
|
||||
($result: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
};
|
||||
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(_) = $result {
|
||||
log::error!(target: "app", "{}", $err_str);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace_err {
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// wrap the anyhow error
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
macro_rules! wrap_err {
|
||||
($stat: expr) => {
|
||||
match $stat {
|
||||
Ok(a) => Ok(a),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{}", err.to_string());
|
||||
Err(format!("{}", err.to_string()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! logging {
|
||||
// 带 println 的版本(支持格式化参数)
|
||||
($level:ident, $type:expr, true, $($arg:tt)*) => {
|
||||
println!("{} {}", $type, format_args!($($arg)*));
|
||||
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||
};
|
||||
|
||||
// 带 println 的版本(使用 false 明确不打印)
|
||||
($level:ident, $type:expr, false, $($arg:tt)*) => {
|
||||
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||
};
|
||||
|
||||
// 不带 print 参数的版本(默认不打印)
|
||||
($level:ident, $type:expr, $($arg:tt)*) => {
|
||||
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! logging_error {
|
||||
// 1. 处理 Result<T, E>,带打印控制
|
||||
($type:expr, $print:expr, $expr:expr) => {
|
||||
match $expr {
|
||||
Ok(_) => {},
|
||||
Err(err) => {
|
||||
if $print {
|
||||
println!("[{}] Error: {}", $type, err);
|
||||
}
|
||||
log::error!(target: "app", "[{}] {}", $type, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 处理 Result<T, E>,默认不打印
|
||||
($type:expr, $expr:expr) => {
|
||||
if let Err(err) = $expr {
|
||||
log::error!(target: "app", "[{}] {}", $type, err);
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 处理格式化字符串,带打印控制
|
||||
($type:expr, $print:expr, $fmt:literal $(, $arg:expr)*) => {
|
||||
if $print {
|
||||
println!("[{}] {}", $type, format_args!($fmt $(, $arg)*));
|
||||
}
|
||||
log::error!(target: "app", "[{}] {}", $type, format_args!($fmt $(, $arg)*));
|
||||
};
|
||||
|
||||
// 4. 处理格式化字符串,不带 bool 时,默认 `false`
|
||||
($type:expr, $fmt:literal $(, $arg:expr)*) => {
|
||||
logging_error!($type, false, $fmt $(, $arg)*);
|
||||
};
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
pub mod dirs;
|
||||
pub mod error;
|
||||
pub mod help;
|
||||
pub mod i18n;
|
||||
pub mod init;
|
||||
pub mod logging;
|
||||
pub mod resolve;
|
||||
pub mod server;
|
||||
pub mod tmpl;
|
||||
|
@ -1,23 +1,19 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::{Config, IVerge, PrfItem},
|
||||
core::*,
|
||||
logging, logging_error,
|
||||
module::lightweight,
|
||||
utils::{error, init, logging::Type, server},
|
||||
wrap_err,
|
||||
};
|
||||
use crate::config::IVerge;
|
||||
use crate::utils::error;
|
||||
use crate::{config::Config, config::PrfItem, core::*, utils::init, utils::server};
|
||||
use crate::{log_err, trace_err, wrap_err};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use serde_yaml::Mapping;
|
||||
use std::net::TcpListener;
|
||||
use tauri::{App, Manager};
|
||||
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
||||
|
||||
use tauri::Url;
|
||||
use url::Url;
|
||||
//#[cfg(not(target_os = "linux"))]
|
||||
// use window_shadows::set_shadow;
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
|
||||
pub static VERSION: OnceCell<String> = OnceCell::new();
|
||||
|
||||
@ -42,207 +38,160 @@ pub fn find_unused_port() -> Result<u16> {
|
||||
pub async fn resolve_setup(app: &mut App) {
|
||||
error::redirect_panic_to_log();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AppHandleManager::global().init(app.app_handle().clone());
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
}
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
let version = app.package_info().version.to_string();
|
||||
|
||||
handle::Handle::global().init(app.app_handle());
|
||||
VERSION.get_or_init(|| version.clone());
|
||||
|
||||
logging_error!(Type::Config, true, init::init_config());
|
||||
logging_error!(Type::Setup, true, init::init_resources());
|
||||
logging_error!(Type::Setup, true, init::init_scheme());
|
||||
logging_error!(Type::Setup, true, init::startup_script().await);
|
||||
log_err!(init::init_config());
|
||||
log_err!(init::init_resources());
|
||||
log_err!(init::init_scheme());
|
||||
log_err!(init::startup_script().await);
|
||||
// 处理随机端口
|
||||
logging_error!(Type::System, true, resolve_random_port_config());
|
||||
log_err!(resolve_random_port_config());
|
||||
// 启动核心
|
||||
logging!(trace, Type::Config, true, "Initial config");
|
||||
logging_error!(Type::Config, true, Config::init_config().await);
|
||||
log::trace!(target:"app", "init config");
|
||||
log_err!(Config::init_config().await);
|
||||
|
||||
logging!(trace, Type::Core, "Starting CoreManager");
|
||||
logging_error!(Type::Core, true, CoreManager::global().init().await);
|
||||
if service::check_service().await.is_err() {
|
||||
match service::reinstall_service().await {
|
||||
Ok(_) => {
|
||||
log::info!(target:"app", "install service susccess.");
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut service_runing = false;
|
||||
for _ in 0..40 {
|
||||
if service::check_service().await.is_ok() {
|
||||
service_runing = true;
|
||||
break;
|
||||
} else {
|
||||
log::warn!(target: "app", "service not runing, sleep 500ms and check again.");
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
if !service_runing {
|
||||
log::error!(target: "app", "service not runing. exit");
|
||||
app.app_handle().exit(-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{e:?}");
|
||||
app.app_handle().exit(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!(target: "app", "launch core");
|
||||
log_err!(CoreManager::global().init().await);
|
||||
|
||||
// setup a simple http server for singleton
|
||||
log::trace!(target: "app", "launch embed server");
|
||||
server::embed_server();
|
||||
|
||||
log::trace!(target: "app", "Initial system tray");
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().init());
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().create_systray(app));
|
||||
log::trace!(target: "app", "init system tray");
|
||||
log_err!(tray::Tray::create_systray());
|
||||
|
||||
logging_error!(
|
||||
Type::System,
|
||||
true,
|
||||
sysopt::Sysopt::global().update_sysproxy().await
|
||||
);
|
||||
logging_error!(
|
||||
Type::System,
|
||||
true,
|
||||
sysopt::Sysopt::global().init_guard_sysproxy()
|
||||
);
|
||||
|
||||
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
|
||||
create_window(!is_silent_start);
|
||||
|
||||
logging_error!(Type::System, true, timer::Timer::global().init());
|
||||
|
||||
let enable_auto_light_weight_mode = { Config::verge().data().enable_auto_light_weight_mode };
|
||||
if enable_auto_light_weight_mode.unwrap_or(false) {
|
||||
lightweight::enable_auto_light_weight_mode();
|
||||
let silent_start = { Config::verge().data().enable_silent_start };
|
||||
if !silent_start.unwrap_or(false) {
|
||||
create_window();
|
||||
}
|
||||
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_part());
|
||||
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
|
||||
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
|
||||
|
||||
// 初始化热键
|
||||
logging!(trace, Type::System, true, "Initial hotkeys");
|
||||
logging_error!(Type::System, true, hotkey::Hotkey::global().init());
|
||||
log_err!(handle::Handle::update_systray_part());
|
||||
log_err!(hotkey::Hotkey::global().init());
|
||||
log_err!(timer::Timer::global().init());
|
||||
}
|
||||
|
||||
/// reset system proxy (异步版)
|
||||
pub async fn resolve_reset_async() {
|
||||
#[cfg(target_os = "macos")]
|
||||
logging!(info, Type::Tray, true, "Unsubscribing from traffic updates");
|
||||
#[cfg(target_os = "macos")]
|
||||
tray::Tray::global().unsubscribe_traffic();
|
||||
|
||||
logging_error!(
|
||||
Type::System,
|
||||
true,
|
||||
sysopt::Sysopt::global().reset_sysproxy().await
|
||||
);
|
||||
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging!(info, Type::System, true, "Restoring system DNS settings");
|
||||
/// reset system proxy
|
||||
pub fn resolve_reset() {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
log_err!(sysopt::Sysopt::global().reset_sysproxy().await);
|
||||
log_err!(CoreManager::global().stop_core().await);
|
||||
#[cfg(target_os = "macos")]
|
||||
restore_public_dns().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// create main window
|
||||
pub fn create_window(is_showup: bool) {
|
||||
logging!(info, Type::Window, true, "Creating window");
|
||||
|
||||
pub fn create_window() {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Found existing window, attempting to restore visibility"
|
||||
);
|
||||
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window is minimized, restoring window state"
|
||||
);
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
trace_err!(window.show(), "set win visible");
|
||||
trace_err!(window.set_focus(), "set win focus");
|
||||
return;
|
||||
}
|
||||
|
||||
logging!(info, Type::Window, true, "Creating new application window");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.inner_size(890.0, 700.0)
|
||||
.min_inner_size(620.0, 550.0)
|
||||
.decorations(false)
|
||||
.maximizable(true)
|
||||
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
|
||||
.transparent(true)
|
||||
.shadow(true)
|
||||
.build();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.decorations(true)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.inner_size(890.0, 700.0)
|
||||
.min_inner_size(620.0, 550.0)
|
||||
.build();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
let builder = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.decorations(false)
|
||||
.inner_size(890.0, 700.0)
|
||||
.min_inner_size(620.0, 550.0)
|
||||
.transparent(true)
|
||||
.build();
|
||||
.visible(false)
|
||||
.fullscreen(false)
|
||||
.min_inner_size(600.0, 520.0);
|
||||
|
||||
match window {
|
||||
Ok(window) => {
|
||||
logging!(info, Type::Window, true, "Window created successfully");
|
||||
if is_showup {
|
||||
println!("is showup");
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
let _ = window.hide();
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
let window = builder
|
||||
.decorations(false)
|
||||
.maximizable(true)
|
||||
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
|
||||
.transparent(true)
|
||||
.visible(false)
|
||||
.build().unwrap();
|
||||
|
||||
// 设置窗口状态监控,实时保存窗口位置和大小
|
||||
// crate::feat::setup_window_state_monitor(&app_handle);
|
||||
#[cfg(target_os = "macos")]
|
||||
let window = builder
|
||||
.decorations(true)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// 标记前端UI已准备就绪,向前端发送启动完成事件
|
||||
let app_handle_clone = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
use tauri::Emitter;
|
||||
#[cfg(target_os = "linux")]
|
||||
let window = builder
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"标记前端UI已准备就绪,开始处理启动错误队列"
|
||||
);
|
||||
handle::Handle::global().mark_startup_completed();
|
||||
|
||||
if let Some(window) = app_handle_clone.get_webview_window("main") {
|
||||
let _ = window.emit("verge://startup-completed", ());
|
||||
}
|
||||
});
|
||||
match window.restore_state(StateFlags::all()) {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "window state restored successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Window,
|
||||
true,
|
||||
"Failed to create window: {:?}",
|
||||
e
|
||||
);
|
||||
log::error!(target: "app", "failed to restore window state: {}", e);
|
||||
#[cfg(target_os = "windows")]
|
||||
window
|
||||
.set_size(tauri::Size::Physical(tauri::PhysicalSize {
|
||||
width: 800,
|
||||
height: 636,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
window
|
||||
.set_size(tauri::Size::Physical(tauri::PhysicalSize {
|
||||
width: 800,
|
||||
height: 642,
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
log::info!(target:"app", "received deep link: {}", param);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
|
||||
let param_str = if param.starts_with("[") && param.len() > 4 {
|
||||
param
|
||||
.get(2..param.len() - 2)
|
||||
@ -265,32 +214,42 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.into_owned());
|
||||
|
||||
// 通过直接获取查询部分并解析特定参数来避免 URL 转义问题
|
||||
let url_param = if let Some(query) = link_parsed.query() {
|
||||
let prefix = "url=";
|
||||
if let Some(pos) = query.find(prefix) {
|
||||
let raw_url = &query[pos + prefix.len()..];
|
||||
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let encode_url = link_parsed
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "url")
|
||||
.map(|(_, value)| value.into_owned());
|
||||
|
||||
match url_param {
|
||||
match encode_url {
|
||||
Some(url) => {
|
||||
log::info!(target:"app", "decoded subscription url: {}", url);
|
||||
let url = percent_decode_str(url.as_ref())
|
||||
.decode_utf8_lossy()
|
||||
.to_string();
|
||||
|
||||
create_window(false);
|
||||
create_window();
|
||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
||||
Ok(item) => {
|
||||
let uid = item.uid.clone().unwrap();
|
||||
let _ = wrap_err!(Config::profiles().data().append_item(item));
|
||||
app_handle
|
||||
.notification()
|
||||
.builder()
|
||||
.title("Clash Verge")
|
||||
.body("Import profile success")
|
||||
.show()
|
||||
.unwrap();
|
||||
|
||||
handle::Handle::notice_message("import_sub_url::ok", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
app_handle
|
||||
.notification()
|
||||
.builder()
|
||||
.title("Clash Verge")
|
||||
.body(format!("Import profile failed: {e}"))
|
||||
.show()
|
||||
.unwrap();
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
bail!("Failed to add subscriptions: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -332,7 +291,8 @@ fn resolve_random_port_config() -> Result<()> {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn set_public_dns(dns_server: String) {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use crate::core::handle;
|
||||
use crate::utils::dirs;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
|
||||
@ -368,7 +328,8 @@ pub async fn set_public_dns(dns_server: String) {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn restore_public_dns() {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use crate::core::handle;
|
||||
use crate::utils::dirs;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
log::info!(target: "app", "try to unset system dns");
|
||||
|
@ -1,11 +1,8 @@
|
||||
extern crate warp;
|
||||
|
||||
use super::resolve;
|
||||
use crate::{
|
||||
config::{Config, IVerge, DEFAULT_PAC},
|
||||
logging_error,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use crate::config::{Config, IVerge, DEFAULT_PAC};
|
||||
use crate::log_err;
|
||||
use anyhow::{bail, Result};
|
||||
use port_scanner::local_port_available;
|
||||
use std::convert::Infallible;
|
||||
@ -49,7 +46,7 @@ pub fn embed_server() {
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let visible = warp::path!("commands" / "visible").map(move || {
|
||||
resolve::create_window(false);
|
||||
resolve::create_window();
|
||||
"ok"
|
||||
});
|
||||
|
||||
@ -70,11 +67,7 @@ pub fn embed_server() {
|
||||
.unwrap_or_default()
|
||||
});
|
||||
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
|
||||
logging_error!(
|
||||
Type::Setup,
|
||||
true,
|
||||
resolve::resolve_scheme(query.param).await
|
||||
);
|
||||
log_err!(resolve::resolve_scheme(query.param).await);
|
||||
Ok("ok")
|
||||
}
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "mihomo_api"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
debug = []
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.44.1", features = ["rt", "macros"] }
|
@ -1,162 +0,0 @@
|
||||
use reqwest::{Method, header::HeaderMap};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
pub mod model;
|
||||
pub use model::{MihomoData, MihomoManager};
|
||||
|
||||
impl MihomoManager {
|
||||
pub fn new(mihomo_server: String, headers: HeaderMap) -> Self {
|
||||
Self {
|
||||
mihomo_server,
|
||||
data: Arc::new(Mutex::new(MihomoData {
|
||||
proxies: serde_json::Value::Null,
|
||||
providers_proxies: serde_json::Value::Null,
|
||||
})),
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_proxies(&self, proxies: serde_json::Value) {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.proxies = proxies;
|
||||
}
|
||||
|
||||
fn update_providers_proxies(&self, providers_proxies: serde_json::Value) {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.providers_proxies = providers_proxies;
|
||||
}
|
||||
|
||||
pub fn get_mihomo_server(&self) -> String {
|
||||
self.mihomo_server.clone()
|
||||
}
|
||||
|
||||
pub fn get_proxies(&self) -> serde_json::Value {
|
||||
let data = self.data.lock().unwrap();
|
||||
data.proxies.clone()
|
||||
}
|
||||
|
||||
pub fn get_providers_proxies(&self) -> serde_json::Value {
|
||||
let data = self.data.lock().unwrap();
|
||||
data.providers_proxies.clone()
|
||||
}
|
||||
|
||||
async fn send_request(
|
||||
&self,
|
||||
method: Method,
|
||||
url: String,
|
||||
data: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let client_response = reqwest::ClientBuilder::new()
|
||||
.default_headers(self.headers.clone())
|
||||
.no_proxy()
|
||||
.timeout(Duration::from_secs(60))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?
|
||||
.request(method.clone(), &url)
|
||||
.json(&data.unwrap_or(json!({})))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let response = match method {
|
||||
Method::PATCH => {
|
||||
let status = client_response.status();
|
||||
if status.as_u16() == 204 {
|
||||
json!({"code": 204})
|
||||
} else {
|
||||
client_response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
}
|
||||
Method::PUT => json!(client_response.text().await.map_err(|e| e.to_string())?),
|
||||
_ => client_response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?,
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn refresh_proxies(&self) -> Result<&Self, String> {
|
||||
let url = format!("{}/proxies", self.mihomo_server);
|
||||
let proxies = self.send_request(Method::GET, url, None).await?;
|
||||
self.update_proxies(proxies);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> {
|
||||
let url = format!("{}/providers/proxies", self.mihomo_server);
|
||||
let providers_proxies = self.send_request(Method::GET, url, None).await?;
|
||||
self.update_providers_proxies(providers_proxies);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MihomoManager {
|
||||
pub async fn is_mihomo_running(&self) -> Result<(), String> {
|
||||
let url = format!("{}/version", self.mihomo_server);
|
||||
let _response = self.send_request(Method::GET, url, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), String> {
|
||||
let url = format!("{}/configs?force=true", self.mihomo_server);
|
||||
let payload = serde_json::json!({
|
||||
"path": clash_config_path,
|
||||
});
|
||||
let _response = self.send_request(Method::PUT, url, Some(payload)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn patch_configs(&self, config: serde_json::Value) -> Result<(), String> {
|
||||
let url = format!("{}/configs", self.mihomo_server);
|
||||
let response = self.send_request(Method::PATCH, url, Some(config)).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_proxy_delay(
|
||||
&self,
|
||||
name: &str,
|
||||
test_url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let test_url = test_url.unwrap_or("http://cp.cloudflare.com/generate_204".to_string());
|
||||
let url = format!(
|
||||
"{}/proxies/{}/delay?url={}&timeout={}",
|
||||
self.mihomo_server, name, test_url, timeout
|
||||
);
|
||||
let response = self.send_request(Method::GET, url, None).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn get_connections(&self) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/connections", self.mihomo_server);
|
||||
let response = self.send_request(Method::GET, url, None).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn delete_connection(&self, id: &str) -> Result<(), String> {
|
||||
let url = format!("{}/connections/{}", self.mihomo_server, id);
|
||||
let response = self.send_request(Method::DELETE, url, None).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
pub struct MihomoData {
|
||||
pub(crate) proxies: serde_json::Value,
|
||||
pub(crate) providers_proxies: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MihomoManager {
|
||||
pub(crate) mihomo_server: String,
|
||||
pub(crate) data: Arc<Mutex<MihomoData>>,
|
||||
pub(crate) headers: HeaderMap,
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
impl Drop for MihomoData {
|
||||
fn drop(&mut self) {
|
||||
println!("Dropping MihomoData");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
impl Drop for MihomoManager {
|
||||
fn drop(&mut self) {
|
||||
println!("Dropping MihomoManager");
|
||||
}
|
||||
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
use mihomo_api;
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
#[test]
|
||||
fn test_mihomo_manager_init() {
|
||||
let manager = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
|
||||
assert_eq!(manager.get_proxies(), serde_json::Value::Null);
|
||||
assert_eq!(manager.get_providers_proxies(), serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_refresh_proxies() {
|
||||
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
|
||||
let manager = manager.refresh_proxies().await.unwrap();
|
||||
let proxies = manager.get_proxies();
|
||||
let providers = manager.get_providers_proxies();
|
||||
assert_ne!(proxies, serde_json::Value::Null);
|
||||
assert_eq!(providers, serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_refresh_providers_proxies() {
|
||||
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
|
||||
let manager = manager.refresh_providers_proxies().await.unwrap();
|
||||
let proxies = manager.get_proxies();
|
||||
let providers = manager.get_providers_proxies();
|
||||
assert_eq!(proxies, serde_json::Value::Null);
|
||||
assert_ne!(providers, serde_json::Value::Null);
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"version": "2.2.3",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
@ -11,19 +10,13 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [
|
||||
"resources",
|
||||
"resources/locales/*"
|
||||
],
|
||||
"resources": ["resources"],
|
||||
"publisher": "Clash Verge Rev",
|
||||
"externalBin": [
|
||||
"sidecar/verge-mihomo",
|
||||
"sidecar/verge-mihomo-alpha"
|
||||
],
|
||||
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
|
||||
"copyright": "GNU General Public License v3.0",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "Clash Verge Rev",
|
||||
"createUpdaterArtifacts": true
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
},
|
||||
"build": {
|
||||
"beforeBuildCommand": "pnpm run web:build",
|
||||
@ -32,43 +25,27 @@
|
||||
"devUrl": "http://localhost:3000/"
|
||||
},
|
||||
"productName": "Clash Verge",
|
||||
"version": "2.0.0",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
||||
"endpoints": [
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
||||
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json"
|
||||
],
|
||||
"windows": {
|
||||
"installMode": "basicUi"
|
||||
}
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
|
||||
]
|
||||
},
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": [
|
||||
"clash",
|
||||
"clash-verge"
|
||||
]
|
||||
"schemes": ["clash", "clash-verge"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"capabilities": [
|
||||
"desktop-capability",
|
||||
"migrated"
|
||||
],
|
||||
"capabilities": ["desktop-capability", "migrated"],
|
||||
"assetProtocol": {
|
||||
"scope": [
|
||||
"$APPDATA/**",
|
||||
"$RESOURCE/../**",
|
||||
"**"
|
||||
],
|
||||
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
|
||||
"enable": true
|
||||
},
|
||||
"csp": null
|
||||
|
@ -30,5 +30,10 @@
|
||||
"./sidecar/verge-mihomo",
|
||||
"./sidecar/verge-mihomo-alpha"
|
||||
]
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"productName": "Clash Verge",
|
||||
"bundle": {
|
||||
"targets": ["app", "dmg"],
|
||||
"resources": ["resources"],
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "10.15",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": "packages/macos/entitlements.plist",
|
||||
"entitlements": null,
|
||||
"dmg": {
|
||||
"background": "images/background.png",
|
||||
"appPosition": {
|
||||
@ -30,5 +30,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon-mono.ico",
|
||||
"iconAsTemplate": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,11 @@
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico",
|
||||
"iconAsTemplate": true,
|
||||
"menuOnLeftClick": false
|
||||
},
|
||||
"windows": []
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user