Compare commits
2266 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b9ccc774 | ||
|
|
d001bbc2eb | ||
|
|
2d1807cd4d | ||
|
|
c6dce2d6cb | ||
|
|
3c7768b379 | ||
|
|
d470bf2dd9 | ||
|
|
b5e8a1ddb5 | ||
|
|
08f3357d9a | ||
|
|
107c54921d | ||
|
|
ca1f7a2b31 | ||
|
|
95e8fe38f2 | ||
|
|
cd6eb26a41 | ||
|
|
9f2c4b4f35 | ||
|
|
adbf98cb15 | ||
|
|
c9b3fec09c | ||
|
|
364fb16b7b | ||
|
|
634ed9cb46 | ||
|
|
cddb117402 | ||
|
|
bd6c4e00c4 | ||
|
|
bbcfb4901f | ||
|
|
230a0eeb64 | ||
|
|
c4c49f61df | ||
|
|
6d5c4f86d3 | ||
|
|
59f22b9cd2 | ||
|
|
6caf2cd057 | ||
|
|
684128ca68 | ||
|
|
6925adc66f | ||
|
|
e1e9d91d86 | ||
|
|
881c136b1e | ||
|
|
5bc699b19c | ||
|
|
52ba1c5a30 | ||
|
|
817d68546e | ||
|
|
c0f5c231ad | ||
|
|
9677d8670b | ||
|
|
b01630d31c | ||
|
|
f771f4720f | ||
|
|
f0becb5003 | ||
|
|
405e8df825 | ||
|
|
49fc7ff7ca | ||
|
|
fde9c8aaee | ||
|
|
8f95c28050 | ||
|
|
2cd97c7785 | ||
|
|
6e48781687 | ||
|
|
dd510b2ee9 | ||
|
|
fadae3f7dc | ||
|
|
fdf8a6b2ba | ||
|
|
4382df7951 | ||
|
|
6cb7d48530 | ||
|
|
9d74b93ee0 | ||
|
|
d370868222 | ||
|
|
9278f9e193 | ||
|
|
a708db6f2b | ||
|
|
d2c4a44297 | ||
|
|
3a24623e4b | ||
|
|
d2084d8d21 | ||
|
|
d9b4ca91e7 | ||
|
|
13da571ad2 | ||
|
|
662d60c0f5 | ||
|
|
368095d2d4 | ||
|
|
496aeeb06d | ||
|
|
b31cbda615 | ||
|
|
7898f3a119 | ||
|
|
ba0a291d97 | ||
|
|
b3e4defc0f | ||
|
|
1ae0ad8a0e | ||
|
|
79b20694c7 | ||
|
|
1ad4941ed8 | ||
|
|
b3fd44d165 | ||
|
|
391a494af6 | ||
|
|
623d075ab8 | ||
|
|
5d70b77316 | ||
|
|
f5f54c0f0e | ||
|
|
91b0b1f279 | ||
|
|
e0fa1f3efe | ||
|
|
b00abf3337 | ||
|
|
a5d846ce4e | ||
|
|
7743c42dd1 | ||
|
|
3f3dab9495 | ||
|
|
2dc9672b20 | ||
|
|
2c90d1ca69 | ||
|
|
1555e2910d | ||
|
|
19b6bd35f5 | ||
|
|
3d4d60edc8 | ||
|
|
db0a000ecb | ||
|
|
24ff48b41c | ||
|
|
18a67d0b2f | ||
|
|
e23af1ad58 | ||
|
|
70af392059 | ||
|
|
d255df23ee | ||
|
|
1317a8b2db | ||
|
|
4cac118442 | ||
|
|
2ce43ccd23 | ||
|
|
d23b2949d8 | ||
|
|
3471476ba2 | ||
|
|
cee61e5619 | ||
|
|
5026e2bade | ||
|
|
83f005c256 | ||
|
|
80843c5ee3 | ||
|
|
ee00defe43 | ||
|
|
7696504d97 | ||
|
|
7a0e38a1b4 | ||
|
|
bc5d577553 | ||
|
|
cfed2b7236 | ||
|
|
e34e6654bc | ||
|
|
d6b85f1a01 | ||
|
|
16af7b53cb | ||
|
|
befc856207 | ||
|
|
5ab8e7a7c7 | ||
|
|
443bfa5928 | ||
|
|
52627575ff | ||
|
|
39973f2d24 | ||
|
|
2b01857d15 | ||
|
|
2650aa845f | ||
|
|
8400a61bf0 | ||
|
|
763b6b9003 | ||
|
|
053f414f3c | ||
|
|
2defa2cc56 | ||
|
|
bb1dbfcfc3 | ||
|
|
1378068a30 | ||
|
|
5d9dce7d10 | ||
|
|
acb66a3012 | ||
|
|
054f902cc6 | ||
|
|
4333153a59 | ||
|
|
19e623d7c2 | ||
|
|
2dd646537e | ||
|
|
1ece079978 | ||
|
|
0d8f779634 | ||
|
|
a7ffc9fc38 | ||
|
|
932b29e66f | ||
|
|
6be7a3b94c | ||
|
|
8e8dd1ec03 | ||
|
|
772f3268ce | ||
|
|
a179591ac2 | ||
|
|
d071e5971f | ||
|
|
a9032f5f20 | ||
|
|
0190702616 | ||
|
|
c35bf38420 | ||
|
|
89d20e564a | ||
|
|
3c24ff3afc | ||
|
|
daf0398750 | ||
|
|
6b14c2b763 | ||
|
|
5bf2f9b8ed | ||
|
|
10a2655288 | ||
|
|
ac07397818 | ||
|
|
30d061d00f | ||
|
|
360d8a5143 | ||
|
|
70e0a5adc8 | ||
|
|
ef52f81494 | ||
|
|
e5c1de3ad3 | ||
|
|
576fd700ae | ||
|
|
476df65a1b | ||
|
|
bd0a4863a8 | ||
|
|
d25fbc05e2 | ||
|
|
b8a0b6f1f4 | ||
|
|
18c7ed1ccc | ||
|
|
b14db06955 | ||
|
|
215dcee3f1 | ||
|
|
37e5c22a5a | ||
|
|
bb0b6c3f77 | ||
|
|
a37df46ce7 | ||
|
|
73fbec2514 | ||
|
|
f66fa08b2c | ||
|
|
bae606bc9d | ||
|
|
1a2d5e988a | ||
|
|
d20bd62b90 | ||
|
|
204bf43b9a | ||
|
|
3ea0d20e2c | ||
|
|
6d527a1cdb | ||
|
|
9c1ece754e | ||
|
|
9070260d41 | ||
|
|
cd8df52aad | ||
|
|
fa86efcdfb | ||
|
|
1a61fab79a | ||
|
|
41a27641df | ||
|
|
a39696151d | ||
|
|
4a4ca5c409 | ||
|
|
5bff7ea469 | ||
|
|
f2cc116ff9 | ||
|
|
80a18c9172 | ||
|
|
a56732e0a3 | ||
|
|
9655f7712b | ||
|
|
13b63b5d96 | ||
|
|
c5989d2735 | ||
|
|
fb4032d6ce | ||
|
|
a29c2d4b14 | ||
|
|
aa7e911c63 | ||
|
|
eeff4d41f4 | ||
|
|
086f023ebc | ||
|
|
b70336c026 | ||
|
|
7d84279370 | ||
|
|
1b2f1b6106 | ||
|
|
fb41c915cc | ||
|
|
bab291a60b | ||
|
|
8461046a4f | ||
|
|
fc925ea032 | ||
|
|
542baf9d69 | ||
|
|
3f02859203 | ||
|
|
af97bd15a9 | ||
|
|
2c081e5a04 | ||
|
|
2d2521e434 | ||
|
|
d9291d4f79 | ||
|
|
0669f7a10b | ||
|
|
f0d953ff59 | ||
|
|
4f6ca40811 | ||
|
|
47848099be | ||
|
|
f839d3bc88 | ||
|
|
800f994b10 | ||
|
|
b149084e39 | ||
|
|
e6580f2f05 | ||
|
|
c5c840d378 | ||
|
|
2e0be4b426 | ||
|
|
460d72f392 | ||
|
|
aa18c4870d | ||
|
|
61533646ad | ||
|
|
b42d13f573 | ||
|
|
0e3b631118 | ||
|
|
38745d4513 | ||
|
|
d22b37c7bf | ||
|
|
d233a84362 | ||
|
|
589324b582 | ||
|
|
c11efcb9be | ||
|
|
6197249377 | ||
|
|
c71e18e97e | ||
|
|
f400f900e6 | ||
|
|
ae5b2cfb79 | ||
|
|
0bb8786ef2 | ||
|
|
f7d5be774d | ||
|
|
c0a0b82fa6 | ||
|
|
277ef51375 | ||
|
|
67b1cf9e1b | ||
|
|
4f797eb7b8 | ||
|
|
29ccabc054 | ||
|
|
857574cbc1 | ||
|
|
acc97f28b5 | ||
|
|
02e32aec41 | ||
|
|
fe0fdc5603 | ||
|
|
66941a18c0 | ||
|
|
f772f92b88 | ||
|
|
d80db8c91e | ||
|
|
a1e67820c7 | ||
|
|
ebe0cfbb0c | ||
|
|
59c52199e3 | ||
|
|
148def4c0f | ||
|
|
3177ea0f4d | ||
|
|
7d65ce847a | ||
|
|
2ac27fcfa7 | ||
|
|
373c8ec0e5 | ||
|
|
7df5ca1912 | ||
|
|
d8dc60ff0d | ||
|
|
56deafee61 | ||
|
|
4c29850e1c | ||
|
|
90bf5e0782 | ||
|
|
c2d07ce4c1 | ||
|
|
564d81fd31 | ||
|
|
f8b7b82ba9 | ||
|
|
71cb2a97c0 | ||
|
|
1cc6472002 | ||
|
|
9b2c80494d | ||
|
|
4ccc2a2961 | ||
|
|
17a8dfb58a | ||
|
|
184b588f20 | ||
|
|
87c4ebe0da | ||
|
|
18a0a15e16 | ||
|
|
c8fd62f388 | ||
|
|
97f03de295 | ||
|
|
2c0a5666fc | ||
|
|
5c3f0d5b60 | ||
|
|
746763ed34 | ||
|
|
e3800a575f | ||
|
|
c78c936762 | ||
|
|
ea5d6f9c46 | ||
|
|
97f8022276 | ||
|
|
d718bd9141 | ||
|
|
8ddd48eda1 | ||
|
|
3e20404959 | ||
|
|
dde7ead751 | ||
|
|
be719680b0 | ||
|
|
d6f5a79ac9 | ||
|
|
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 | ||
|
|
26dd097962 | ||
|
|
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 | ||
|
|
eb1a1b3786 | ||
|
|
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 |
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
|||||||
github: clash-verge-rev
|
|
||||||
34
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -12,16 +12,14 @@ body:
|
|||||||
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
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下进行讨论
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||||
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
|
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) 版本,确定问题是否仍然存在
|
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
||||||
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
|
|
||||||
## Before submitting the issue, please make sure of the following checklist:
|
## 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)
|
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
|
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
|
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
|
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 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
|
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
|
||||||
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
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
@@ -57,16 +55,20 @@ body:
|
|||||||
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: checkboxes
|
||||||
|
id: os-labels
|
||||||
attributes:
|
attributes:
|
||||||
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
label: 系统标签 / OS Labels
|
||||||
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")
|
description: 请选择受影响的操作系统(至少选择一个) / Please select the affected operating system(s) (select at least one)
|
||||||
value: |
|
options:
|
||||||
<details><summary>日志内容 / Log Content</summary>
|
- label: windows
|
||||||
```log
|
- label: macos
|
||||||
<!-- 在此处粘贴完整日志 / Paste the full log here -->
|
- label: linux
|
||||||
|
validations:
|
||||||
```
|
required: true
|
||||||
</details>
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 日志 / Log
|
||||||
|
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug/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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
58
.github/ISSUE_TEMPLATE/i18n_request.yml
vendored
@@ -1,58 +0,0 @@
|
|||||||
name: I18N / 多语言相关
|
|
||||||
title: "[I18N] "
|
|
||||||
description: 用于多语言翻译、国际化相关问题或建议 / For issues or suggestions related to translations and internationalization
|
|
||||||
labels: ["I18n"]
|
|
||||||
type: "Task"
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
## I18N 相关问题/建议
|
|
||||||
请用此模板提交翻译错误、缺失、建议或新增语言请求。
|
|
||||||
Please use this template for translation errors, missing translations, suggestions, or new language requests.
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: 问题描述 / Description
|
|
||||||
description: 详细描述你的 I18N 问题或建议 / Please describe your I18N issue or suggestion in detail
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: language
|
|
||||||
attributes:
|
|
||||||
label: 相关语言 / Language
|
|
||||||
description: 例如 zh, en, jp, ru, ... / e.g. zh, en, jp, ru, ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: suggestion
|
|
||||||
attributes:
|
|
||||||
label: 建议或修正内容 / Suggestion or Correction
|
|
||||||
description: 如果是翻译修正或建议,请填写建议的内容 / If this is a translation correction or suggestion, please provide the suggested content
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: i18n-type
|
|
||||||
attributes:
|
|
||||||
label: 问题类型 / Issue Type
|
|
||||||
description: 请选择适用类型(可多选) / Please select the applicable type(s)
|
|
||||||
options:
|
|
||||||
- label: 翻译错误 / Translation error
|
|
||||||
- label: 翻译缺失 / Missing translation
|
|
||||||
- label: 建议优化 / Suggestion
|
|
||||||
- label: 新增语言 / New language
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: verge-version
|
|
||||||
attributes:
|
|
||||||
label: 软件版本 / Verge Version
|
|
||||||
description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
400
.github/workflows/alpha.yml
vendored
@@ -1,262 +1,54 @@
|
|||||||
name: Alpha Build
|
name: Alpha Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境
|
|
||||||
# 所以不再使用 workflow_dispatch 触发
|
|
||||||
# 应当通过 git tag 来触发构建
|
|
||||||
# TODO 手动控制版本号
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
# inputs:
|
schedule:
|
||||||
# tag_name:
|
# UTC+8 00:00 (UTC 16:00 previous day) and UTC+8 12:00 (UTC 04:00)
|
||||||
# description: "Alpha tag name (e.g. v1.2.3-alpha.1)"
|
- cron: "0 16,4 * * *"
|
||||||
# required: true
|
|
||||||
# type: string
|
|
||||||
|
|
||||||
# push:
|
|
||||||
# # 应当限制在 dev 分支上触发发布。
|
|
||||||
# branches:
|
|
||||||
# - dev
|
|
||||||
# # 应当限制 v*.*.*-alpha* 的 tag 来触发发布。
|
|
||||||
# tags:
|
|
||||||
# - "v*.*.*-alpha*"
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
TAG_NAME: alpha
|
|
||||||
TAG_CHANNEL: Alpha
|
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: short
|
||||||
concurrency:
|
concurrency:
|
||||||
|
# only allow per workflow per commit (and not pr) to run at a time
|
||||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_alpha_tag:
|
check_commit:
|
||||||
name: Check Alpha Tag package.json Version Consistency
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check tag and package.json version
|
|
||||||
id: check_tag
|
|
||||||
run: |
|
|
||||||
TAG_REF="${GITHUB_REF##*/}"
|
|
||||||
echo "Current tag: $TAG_REF"
|
|
||||||
if [[ ! "$TAG_REF" =~ -alpha ]]; then
|
|
||||||
echo "Current tag is not an alpha tag."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
PKG_VERSION=$(jq -r .version package.json)
|
|
||||||
echo "package.json version: $PKG_VERSION"
|
|
||||||
if [[ "$PKG_VERSION" != *alpha* ]]; then
|
|
||||||
echo "package.json version is not an alpha version."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
|
|
||||||
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Alpha tag and package.json version are consistent."
|
|
||||||
|
|
||||||
delete_old_assets:
|
|
||||||
name: Delete Old Alpha Release Assets and Tags
|
|
||||||
needs: check_alpha_tag
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Delete Old Alpha Tags Except Latest
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
fetch-depth: 2
|
||||||
script: |
|
|
||||||
const tagPattern = /-alpha.*/; // 匹配带有 -alpha 的 tag
|
|
||||||
const owner = context.repo.owner;
|
|
||||||
const repo = context.repo.repo;
|
|
||||||
|
|
||||||
try {
|
- name: Check if commit changed
|
||||||
// 获取所有 tag
|
id: check
|
||||||
const { data: tags } = await github.rest.repos.listTags({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
per_page: 100 // 调整 per_page 以获取更多 tag
|
|
||||||
});
|
|
||||||
|
|
||||||
// 过滤出包含 -alpha 的 tag
|
|
||||||
const alphaTags = (await Promise.all(
|
|
||||||
tags
|
|
||||||
.filter(tag => tagPattern.test(tag.name))
|
|
||||||
.map(async tag => {
|
|
||||||
// 获取每个 tag 的 commit 信息以获得日期
|
|
||||||
const { data: commit } = await github.rest.repos.getCommit({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
ref: tag.commit.sha
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...tag,
|
|
||||||
commitDate: commit.committer && commit.committer.date ? commit.committer.date : commit.commit.author.date
|
|
||||||
};
|
|
||||||
})
|
|
||||||
)).sort((a, b) => {
|
|
||||||
// 按 commit 日期降序排序(最新的在前面)
|
|
||||||
return new Date(b.commitDate) - new Date(a.commitDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Found ${alphaTags.length} alpha tags`);
|
|
||||||
|
|
||||||
if (alphaTags.length === 0) {
|
|
||||||
console.log('No alpha tags found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保留最新的 tag
|
|
||||||
const latestTag = alphaTags[0];
|
|
||||||
console.log(`Keeping latest alpha tag: ${latestTag.name}`);
|
|
||||||
|
|
||||||
// 处理其他旧的 alpha tag
|
|
||||||
for (const tag of alphaTags.slice(1)) {
|
|
||||||
console.log(`Processing tag: ${tag.name}`);
|
|
||||||
|
|
||||||
// 获取与 tag 关联的 release
|
|
||||||
try {
|
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
tag: tag.name
|
|
||||||
});
|
|
||||||
|
|
||||||
// 删除 release 下的所有资产
|
|
||||||
if (release.assets && release.assets.length > 0) {
|
|
||||||
console.log(`Deleting ${release.assets.length} assets for release ${tag.name}`);
|
|
||||||
for (const asset of release.assets) {
|
|
||||||
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
|
|
||||||
await github.rest.repos.deleteReleaseAsset({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
asset_id: asset.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除 release
|
|
||||||
console.log(`Deleting release for tag: ${tag.name}`);
|
|
||||||
await github.rest.repos.deleteRelease({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
release_id: release.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// 删除 tag
|
|
||||||
console.log(`Deleting tag: ${tag.name}`);
|
|
||||||
await github.rest.git.deleteRef({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
ref: `tags/${tag.name}`
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status === 404) {
|
|
||||||
console.log(`No release found for tag ${tag.name}, deleting tag directly`);
|
|
||||||
await github.rest.git.deleteRef({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
ref: `tags/${tag.name}`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error(`Error processing tag ${tag.name}:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Old alpha tags and releases deleted successfully');
|
|
||||||
} catch (error) {
|
|
||||||
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 UPDATE logs
|
|
||||||
id: fetch_update_logs
|
|
||||||
run: |
|
run: |
|
||||||
if [ -f "UPDATELOG.md" ]; then
|
# For manual workflow_dispatch, always run
|
||||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
if [ -n "$UPDATE_LOGS" ]; then
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
echo "Found update logs"
|
exit 0
|
||||||
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
|
||||||
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
|
||||||
echo "EOF" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "No update 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: |
|
|
||||||
if [ -z "$UPDATE_LOGS" ]; then
|
|
||||||
echo "No update logs found, using default message"
|
|
||||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
|
||||||
else
|
|
||||||
echo "Using found update logs"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > release.txt << EOF
|
# Check if current commit is different from the previous one
|
||||||
$UPDATE_LOGS
|
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||||
|
PREVIOUS_COMMIT=$(git rev-parse HEAD~1)
|
||||||
|
|
||||||
## 我应该下载哪个版本?
|
if [ "$CURRENT_COMMIT" != "$PREVIOUS_COMMIT" ]; then
|
||||||
|
echo "New commit detected: $CURRENT_COMMIT"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "No new commits since last run"
|
||||||
|
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
### MacOS
|
alpha:
|
||||||
- MacOS intel芯片: x64.dmg
|
needs: check_commit
|
||||||
- MacOS apple M芯片: aarch64.dmg
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
|
|
||||||
### 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: ${{ env.TAG_NAME }}
|
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
|
||||||
body_path: release.txt
|
|
||||||
prerelease: true
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
generate_release_notes: true
|
|
||||||
|
|
||||||
alpha-x86-windows-macos-linux:
|
|
||||||
name: Alpha x86 Windows, MacOS and Linux
|
|
||||||
needs: update_tag
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -271,6 +63,7 @@ jobs:
|
|||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
@@ -286,13 +79,14 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
save-if: false
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
- name: Install dependencies (ubuntu only)
|
||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
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
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -307,10 +101,10 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
- name: Alpha Version update
|
||||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
run: pnpm run fix-alpha-version
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -326,17 +120,17 @@ jobs:
|
|||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: ${{ env.TAG_NAME }}
|
tagName: alpha
|
||||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
releaseName: "Clash Verge Rev Alpha"
|
||||||
releaseBody: "More new features are now supported."
|
releaseBody: "More new features are now supported."
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tauriScript: pnpm
|
tauriScript: pnpm
|
||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
alpha-arm-linux:
|
alpha-for-linux-arm:
|
||||||
name: Alpha ARM Linux
|
needs: check_commit
|
||||||
needs: update_tag
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -362,7 +156,7 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
save-if: false
|
cache-all-crates: true
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -377,13 +171,10 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
- name: "Setup for linux"
|
||||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
run: |-
|
||||||
|
|
||||||
- name: Setup for linux
|
|
||||||
run: |
|
|
||||||
sudo ls -lR /etc/apt/
|
sudo ls -lR /etc/apt/
|
||||||
|
|
||||||
cat > /tmp/sources.list << EOF
|
cat > /tmp/sources.list << EOF
|
||||||
@@ -402,29 +193,23 @@ jobs:
|
|||||||
sudo mv /tmp/sources.list /etc/apt/sources.list
|
sudo mv /tmp/sources.list /etc/apt/sources.list
|
||||||
|
|
||||||
sudo dpkg --add-architecture ${{ matrix.arch }}
|
sudo dpkg --add-architecture ${{ matrix.arch }}
|
||||||
sudo apt-get update -y
|
sudo apt update
|
||||||
sudo apt-get -f install -y
|
|
||||||
|
|
||||||
sudo apt-get install -y \
|
sudo apt install -y \
|
||||||
linux-libc-dev:${{ matrix.arch }} \
|
|
||||||
libc6-dev:${{ matrix.arch }}
|
|
||||||
|
|
||||||
sudo apt-get install -y \
|
|
||||||
libxslt1.1:${{ matrix.arch }} \
|
|
||||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
libssl-dev:${{ matrix.arch }} \
|
libssl-dev:${{ matrix.arch }} \
|
||||||
patchelf:${{ matrix.arch }} \
|
patchelf:${{ matrix.arch }} \
|
||||||
librsvg2-dev:${{ matrix.arch }}
|
librsvg2-dev:${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Install aarch64 tools
|
- name: "Install aarch64 tools"
|
||||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||||
run: |
|
run: |
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
g++-aarch64-linux-gnu
|
g++-aarch64-linux-gnu
|
||||||
|
|
||||||
- name: Install armv7 tools
|
- name: "Install armv7 tools"
|
||||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||||
run: |
|
run: |
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
@@ -457,17 +242,18 @@ jobs:
|
|||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: alpha
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: "Clash Verge Rev Alpha"
|
||||||
|
body: "More new features are now supported."
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||||
|
|
||||||
alpha-x86-arm-windows_webview2:
|
alpha-for-fixed-webview2:
|
||||||
name: Alpha x86 and ARM Windows with WebView2
|
needs: check_commit
|
||||||
needs: update_tag
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -490,7 +276,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
save-if: false
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -505,10 +292,7 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
|
||||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
|
||||||
|
|
||||||
- name: Download WebView2 Runtime
|
- name: Download WebView2 Runtime
|
||||||
run: |
|
run: |
|
||||||
@@ -552,13 +336,77 @@ jobs:
|
|||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: alpha
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: "Clash Verge Rev Alpha"
|
||||||
|
body: "More new features are now supported."
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
|
|
||||||
- name: Portable Bundle
|
- name: Portable Bundle
|
||||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
|
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
update_tag:
|
||||||
|
name: Update tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [check_commit, alpha, alpha-for-linux-arm, alpha-for-fixed-webview2]
|
||||||
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
|
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)
|
||||||
|
#### 正常版本(推荐)
|
||||||
|
- 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
|
||||||
|
|||||||
485
.github/workflows/autobuild.yml
vendored
@@ -1,485 +0,0 @@
|
|||||||
name: Auto Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# schedule:
|
|
||||||
# # UTC+8 0,6,12,18
|
|
||||||
# - cron: "0 16,22,4,10 * * *"
|
|
||||||
permissions: write-all
|
|
||||||
env:
|
|
||||||
TAG_NAME: autobuild
|
|
||||||
TAG_CHANNEL: AutoBuild
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
RUST_BACKTRACE: short
|
|
||||||
concurrency:
|
|
||||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
|
||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check_commit:
|
|
||||||
name: Check Commit Needs Build
|
|
||||||
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: |
|
|
||||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
|
||||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
|
||||||
echo "Current version: $CURRENT_VERSION"
|
|
||||||
|
|
||||||
git checkout HEAD~1 package.json
|
|
||||||
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
|
||||||
echo "Previous version: $PREVIOUS_VERSION"
|
|
||||||
|
|
||||||
git checkout HEAD package.json
|
|
||||||
|
|
||||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
|
||||||
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
|
||||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
update_tag:
|
|
||||||
name: Update tag
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: check_commit
|
|
||||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Update git tag
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git tag -f ${{ env.TAG_NAME }}
|
|
||||||
git push --force origin ${{ env.TAG_NAME }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Fetch UPDATE logs
|
|
||||||
id: fetch_update_logs
|
|
||||||
run: |
|
|
||||||
if [ -f "UPDATELOG.md" ]; then
|
|
||||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
|
||||||
if [ -n "$UPDATE_LOGS" ]; then
|
|
||||||
echo "Found update logs"
|
|
||||||
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
|
||||||
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
|
||||||
echo "EOF" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "No update 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: |
|
|
||||||
if [ -z "$UPDATE_LOGS" ]; then
|
|
||||||
echo "No update logs found, using default message"
|
|
||||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
|
||||||
else
|
|
||||||
echo "Using found update logs"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > release.txt << EOF
|
|
||||||
$UPDATE_LOGS
|
|
||||||
|
|
||||||
## Which version should I download?
|
|
||||||
|
|
||||||
### MacOS
|
|
||||||
- MacOS Intel Chip: x64.dmg
|
|
||||||
- MacOS Apple M Chip: aarch64.dmg
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
- Linux 64-bit: amd64.deb/amd64.rpm
|
|
||||||
- Linux arm64: arm64.deb/aarch64.rpm
|
|
||||||
- Linux armv7: armhf.deb/armhfp.rpm
|
|
||||||
|
|
||||||
### Windows (Win7 is no longer supported)
|
|
||||||
#### Normal version (recommended)
|
|
||||||
- 64-bit: x64-setup.exe
|
|
||||||
- arm64: arm64-setup.exe
|
|
||||||
#### Portable version is no longer available with many problems
|
|
||||||
#### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
|
|
||||||
- 64-bit: x64_fixed_webview2-setup.exe
|
|
||||||
- arm64: arm64_fixed_webview2-setup.exe
|
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Upload Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
|
||||||
body_path: release.txt
|
|
||||||
prerelease: true
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
generate_release_notes: true
|
|
||||||
|
|
||||||
clean_old_assets:
|
|
||||||
name: Clean Old Release Assets
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: update_tag
|
|
||||||
if: ${{ needs.update_tag.result == 'success' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Remove old assets from release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAG_NAME: ${{ env.TAG_NAME }}
|
|
||||||
run: |
|
|
||||||
VERSION=$(cat package.json | jq -r '.version')
|
|
||||||
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
|
|
||||||
for asset in $assets; do
|
|
||||||
if [[ "$asset" != *"$VERSION"* ]]; then
|
|
||||||
echo "Deleting old asset: $asset"
|
|
||||||
gh release delete-asset "$TAG_NAME" "$asset" -y
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
autobuild-x86-windows-macos-linux:
|
|
||||||
name: Autobuild x86 Windows, MacOS and Linux
|
|
||||||
needs: update_tag
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: windows-latest
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
- os: windows-latest
|
|
||||||
target: aarch64-pc-windows-msvc
|
|
||||||
- os: macos-latest
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
- os: macos-latest
|
|
||||||
target: x86_64-apple-darwin
|
|
||||||
- os: ubuntu-22.04
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust Stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Add Rust Target
|
|
||||||
run: rustup target add ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
cache-all-crates: true
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
name: Install pnpm
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Pnpm install and check
|
|
||||||
run: |
|
|
||||||
pnpm i
|
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
|
||||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
|
||||||
|
|
||||||
- name: Tauri build
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
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 }}
|
|
||||||
with:
|
|
||||||
tagName: ${{ env.TAG_NAME }}
|
|
||||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
|
||||||
releaseBody: "More new features are now supported."
|
|
||||||
releaseDraft: false
|
|
||||||
prerelease: true
|
|
||||||
tauriScript: pnpm
|
|
||||||
args: --target ${{ matrix.target }}
|
|
||||||
|
|
||||||
autobuild-arm-linux:
|
|
||||||
name: Autobuild ARM Linux
|
|
||||||
needs: update_tag
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: ubuntu-22.04
|
|
||||||
target: aarch64-unknown-linux-gnu
|
|
||||||
arch: arm64
|
|
||||||
- os: ubuntu-22.04
|
|
||||||
target: armv7-unknown-linux-gnueabihf
|
|
||||||
arch: armhf
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust Stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Add Rust Target
|
|
||||||
run: rustup target add ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
cache-all-crates: true
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Pnpm install and check
|
|
||||||
run: |
|
|
||||||
pnpm i
|
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
|
||||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
|
||||||
|
|
||||||
- name: Setup for linux
|
|
||||||
run: |
|
|
||||||
sudo ls -lR /etc/apt/
|
|
||||||
|
|
||||||
cat > /tmp/sources.list << EOF
|
|
||||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
|
|
||||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
|
|
||||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
|
|
||||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
|
|
||||||
|
|
||||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
|
|
||||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
|
|
||||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
|
|
||||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
|
|
||||||
sudo mv /tmp/sources.list /etc/apt/sources.list
|
|
||||||
|
|
||||||
sudo dpkg --add-architecture ${{ matrix.arch }}
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get -f install -y
|
|
||||||
|
|
||||||
sudo apt-get install -y \
|
|
||||||
linux-libc-dev:${{ matrix.arch }} \
|
|
||||||
libc6-dev:${{ matrix.arch }}
|
|
||||||
|
|
||||||
sudo apt-get 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
|
|
||||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
|
||||||
run: |
|
|
||||||
sudo apt install -y \
|
|
||||||
gcc-aarch64-linux-gnu \
|
|
||||||
g++-aarch64-linux-gnu
|
|
||||||
|
|
||||||
- name: Install armv7 tools
|
|
||||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
|
||||||
run: |
|
|
||||||
sudo apt install -y \
|
|
||||||
gcc-arm-linux-gnueabihf \
|
|
||||||
g++-arm-linux-gnueabihf
|
|
||||||
|
|
||||||
- name: Build for Linux
|
|
||||||
run: |
|
|
||||||
export PKG_CONFIG_ALLOW_CROSS=1
|
|
||||||
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
|
|
||||||
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
|
||||||
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
|
|
||||||
elif [ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]; then
|
|
||||||
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/:$PKG_CONFIG_PATH
|
|
||||||
export PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf/
|
|
||||||
fi
|
|
||||||
pnpm build --target ${{ matrix.target }}
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Get Version
|
|
||||||
run: |
|
|
||||||
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
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
|
||||||
prerelease: true
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
files: |
|
|
||||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
|
||||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
|
||||||
|
|
||||||
autobuild-x86-arm-windows_webview2:
|
|
||||||
name: Autobuild x86 and ARM Windows with WebView2
|
|
||||||
needs: update_tag
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: windows-latest
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
arch: x64
|
|
||||||
- os: windows-latest
|
|
||||||
target: aarch64-pc-windows-msvc
|
|
||||||
arch: arm64
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Add Rust Target
|
|
||||||
run: rustup target add ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
cache-all-crates: true
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Pnpm install and check
|
|
||||||
run: |
|
|
||||||
pnpm i
|
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
|
||||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
|
||||||
|
|
||||||
- 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
|
|
||||||
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
|
|
||||||
Remove-Item .\src-tauri\tauri.windows.conf.json
|
|
||||||
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
|
||||||
|
|
||||||
- name: Tauri build
|
|
||||||
id: build
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
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 }}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Upload Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
|
||||||
prerelease: true
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
|
||||||
|
|
||||||
- name: Portable Bundle
|
|
||||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
63
.github/workflows/clippy.yml
vendored
@@ -1,63 +0,0 @@
|
|||||||
name: Clippy Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
clippy:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: windows-latest
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
- os: macos-latest
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
- os: ubuntu-22.04
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust Stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Add Rust Target
|
|
||||||
run: rustup target add ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
save-if: false
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Pnpm install and check
|
|
||||||
run: |
|
|
||||||
pnpm i
|
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Build Web Assets
|
|
||||||
run: pnpm run web:build
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
|
||||||
|
|
||||||
- name: Run Clippy
|
|
||||||
run: cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings
|
|
||||||
64
.github/workflows/cross_check.yaml
vendored
@@ -1,64 +0,0 @@
|
|||||||
name: Cross Platform Cargo Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# pull_request:
|
|
||||||
# push:
|
|
||||||
# branches: [main, dev]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cargo-check:
|
|
||||||
# Treat all Rust compiler warnings as errors
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: "-D warnings"
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: macos-latest
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
- os: windows-latest
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
- os: ubuntu-latest
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust Stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Add Rust Target
|
|
||||||
run: rustup target add ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
name: Install pnpm
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Pnpm install and check
|
|
||||||
run: |
|
|
||||||
pnpm i
|
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
save-if: false
|
|
||||||
|
|
||||||
- name: Cargo Check (deny warnings)
|
|
||||||
working-directory: src-tauri
|
|
||||||
run: |
|
|
||||||
cargo check --target ${{ matrix.target }} --workspace --all-features
|
|
||||||
5
.github/workflows/dev.yml
vendored
@@ -42,7 +42,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
save-if: false
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -57,7 +58,7 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
|||||||
50
.github/workflows/fmt.yml
vendored
@@ -1,50 +0,0 @@
|
|||||||
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
name: Check Formatting
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rustfmt:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: install Rust stable and rustfmt
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: rustfmt
|
|
||||||
|
|
||||||
- name: run cargo fmt
|
|
||||||
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
|
|
||||||
|
|
||||||
prettier:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "lts/*"
|
|
||||||
- run: corepack enable
|
|
||||||
- run: pnpm install --frozen-lockfile
|
|
||||||
- run: pnpm format:check
|
|
||||||
|
|
||||||
# taplo:
|
|
||||||
# name: taplo (.toml files)
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# steps:
|
|
||||||
# - uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# - name: install Rust stable
|
|
||||||
# uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
# - name: install taplo-cli
|
|
||||||
# uses: taiki-e/install-action@v2
|
|
||||||
# with:
|
|
||||||
# tool: taplo-cli
|
|
||||||
|
|
||||||
# - run: taplo fmt --check --diff
|
|
||||||
155
.github/workflows/release.yml
vendored
@@ -1,16 +1,7 @@
|
|||||||
name: Release Build
|
name: Release Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# ! 为了避免重复发布版本,应当通过独特 git tag 触发。
|
workflow_dispatch:
|
||||||
# ! 不再使用 workflow_dispatch 触发。
|
|
||||||
# workflow_dispatch:
|
|
||||||
push:
|
|
||||||
# 应当限制在 main 分支上触发发布。
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
# 应当限制 v*.*.* 的 tag 触发发布。
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
@@ -21,28 +12,7 @@ concurrency:
|
|||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_tag_version:
|
|
||||||
name: Check Release Tag and package.json Version Consistency
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Check tag and package.json version
|
|
||||||
run: |
|
|
||||||
TAG_REF="${GITHUB_REF##*/}"
|
|
||||||
echo "Current tag: $TAG_REF"
|
|
||||||
PKG_VERSION=$(jq -r .version package.json)
|
|
||||||
echo "package.json version: $PKG_VERSION"
|
|
||||||
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
|
|
||||||
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Tag and package.json version are consistent."
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release Build
|
|
||||||
needs: check_tag_version
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -73,13 +43,14 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
save-if: false
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
- name: Install dependencies (ubuntu only)
|
||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
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
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -94,7 +65,7 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -103,14 +74,20 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_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 }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: v__VERSION__
|
tagName: v__VERSION__
|
||||||
releaseName: "Clash Verge Rev Lite v__VERSION__"
|
releaseName: "Clash Verge Rev v__VERSION__"
|
||||||
|
releaseBody: "More new features are now supported."
|
||||||
tauriScript: pnpm
|
tauriScript: pnpm
|
||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
release-for-linux-arm:
|
release-for-linux-arm:
|
||||||
name: Release Build for Linux ARM
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -136,7 +113,7 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
save-if: false
|
cache-all-crates: true
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -151,7 +128,7 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
- name: "Setup for linux"
|
- name: "Setup for linux"
|
||||||
run: |-
|
run: |-
|
||||||
@@ -176,7 +153,6 @@ jobs:
|
|||||||
sudo apt update
|
sudo apt update
|
||||||
|
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
libxslt1.1:${{ matrix.arch }} \
|
|
||||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
libssl-dev:${{ matrix.arch }} \
|
libssl-dev:${{ matrix.arch }} \
|
||||||
@@ -218,20 +194,20 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install jq
|
sudo apt-get install jq
|
||||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: v${{env.VERSION}}
|
tag_name: v${{env.VERSION}}
|
||||||
name: "Clash Verge Rev Lite v${{env.VERSION}}"
|
name: "Clash Verge Rev v${{env.VERSION}}"
|
||||||
|
body: "More new features are now supported."
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||||
|
|
||||||
release-for-fixed-webview2:
|
release-for-fixed-webview2:
|
||||||
name: Release Build for Fixed WebView2
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -254,7 +230,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
save-if: false
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -269,12 +246,12 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Download WebView2 Runtime
|
- name: Download WebView2 Runtime
|
||||||
run: |
|
run: |
|
||||||
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
|
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
|
||||||
Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
|
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
|
||||||
Remove-Item .\src-tauri\tauri.windows.conf.json
|
Remove-Item .\src-tauri\tauri.windows.conf.json
|
||||||
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
||||||
|
|
||||||
@@ -314,7 +291,8 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: v${{steps.build.outputs.appVersion}}
|
tag_name: v${{steps.build.outputs.appVersion}}
|
||||||
name: "Clash Verge Rev Lite v${{steps.build.outputs.appVersion}}"
|
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
|
||||||
|
body: "More new features are now supported."
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
|
|
||||||
@@ -324,7 +302,6 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
release-update:
|
release-update:
|
||||||
name: Release Update
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [release, release-for-linux-arm]
|
needs: [release, release-for-linux-arm]
|
||||||
steps:
|
steps:
|
||||||
@@ -374,84 +351,24 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
create_release_notes:
|
submit-to-winget:
|
||||||
name: Create Release Notes
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release-update]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
- name: Fetch UPDATE logs
|
fetch-depth: 0
|
||||||
id: fetch_update_logs
|
|
||||||
run: |
|
|
||||||
if [ -f "UPDATELOG.md" ]; then
|
|
||||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
|
||||||
if [ -n "$UPDATE_LOGS" ]; then
|
|
||||||
echo "Found update logs"
|
|
||||||
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
|
||||||
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
|
||||||
echo "EOF" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "No update sections found in UPDATELOG.md"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "UPDATELOG.md file not found"
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Get Version
|
- name: Get Version
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install jq
|
sudo apt-get install jq
|
||||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
|
- name: Submit to Winget
|
||||||
|
uses: vedantmgoyal9/winget-releaser@main
|
||||||
- run: |
|
|
||||||
if [ -z "$UPDATE_LOGS" ]; then
|
|
||||||
echo "No update logs found, using default message"
|
|
||||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
|
||||||
else
|
|
||||||
echo "Using found update logs"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > release.txt << EOF
|
|
||||||
$UPDATE_LOGS
|
|
||||||
|
|
||||||
## Which version should I download?
|
|
||||||
|
|
||||||
### macOS
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_aarch64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Apple%20Silicon"></a><br>
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_x64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Intel"></a><br>
|
|
||||||
> :warning: **Warning**
|
|
||||||
If you get a notification that the application is corrupted when you run it on macOS, run this command:<br>
|
|
||||||
`sudo xattr -r -c /Applications/Clash\ Verge\ Rev\ Lite.app`
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_amd64.deb"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=debian&label=DEB"> </a><br>
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite-${{ env.VERSION }}-1.x86_64.rpm"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=fedora&label=RPM"> </a>
|
|
||||||
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_arm64.deb"><img src="https://img.shields.io/badge/arm64-default?style=flat&logo=debian&label=DEB"> </a><br>
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite-${{ env.VERSION }}-1.aarch64.rpm"><img src="https://img.shields.io/badge/aarch64-default?style=flat&logo=fedora&label=RPM"> </a>
|
|
||||||
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_armhf.deb"><img src="https://img.shields.io/badge/armhf-default?style=flat&logo=debian&label=DEB"> </a><br>
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite-${{ env.VERSION }}-1.armhfp.rpm"><img src="https://img.shields.io/badge/armhfp-default?style=flat&logo=fedora&label=RPM"> </a>
|
|
||||||
|
|
||||||
### Windows (Win7 is no longer supported)
|
|
||||||
#### Normal version (recommended)
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_x64-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_arm64-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
|
|
||||||
#### Portable version is no longer available with many problems
|
|
||||||
#### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_x64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
|
|
||||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
|
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Upload Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
with:
|
||||||
tag_name: v${{env.VERSION}}
|
identifier: ClashVergeRev.ClashVergeRev
|
||||||
name: "Clash Verge Rev Lite v${{env.VERSION}}"
|
version: ${{env.VERSION}}
|
||||||
body_path: release.txt
|
release-tag: v${{env.VERSION}}
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -9,4 +9,3 @@ scripts/_env.sh
|
|||||||
.vscode
|
.vscode
|
||||||
.tool-versions
|
.tool-versions
|
||||||
.idea
|
.idea
|
||||||
.old
|
|
||||||
|
|||||||
@@ -1,26 +1 @@
|
|||||||
#!/bin/bash
|
pnpm pretty-quick --staged
|
||||||
|
|
||||||
#pnpm pretty-quick --staged
|
|
||||||
|
|
||||||
if git diff --cached --name-only | grep -q '^src/'; then
|
|
||||||
pnpm format:check
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Code format check failed in src/. Please fix formatting issues."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
|
||||||
cd src-tauri
|
|
||||||
cargo fmt
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "rustfmt failed to format the code. Please fix the issues and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
cd ..
|
|
||||||
fi
|
|
||||||
|
|
||||||
#git add .
|
|
||||||
|
|
||||||
# 允许提交
|
|
||||||
exit 0
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# $1: remote name (e.g., origin)
|
|
||||||
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
|
|
||||||
|
|
||||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
|
||||||
cargo clippy --manifest-path ./src-tauri/Cargo.toml
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Clippy found issues in src-tauri. Please fix them before pushing."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 只在 push 到 origin 并且 origin 指向目标仓库时执行格式检查
|
|
||||||
if [ "$1" = "origin" ] && echo "$2" | grep -Eq 'github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$'; then
|
|
||||||
echo "[pre-push] Detected push to origin (clash-verge-rev/clash-verge-rev)"
|
|
||||||
echo "[pre-push] Running pnpm format:check..."
|
|
||||||
|
|
||||||
pnpm format:check
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "❌ Code format check failed. Please fix formatting before pushing."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[pre-push] Not pushing to target repo. Skipping format check."
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# README.md
|
|
||||||
# UPDATELOG.md
|
|
||||||
# CONTRIBUTING.md
|
|
||||||
|
|
||||||
pnpm-lock.yaml
|
|
||||||
|
|
||||||
src-tauri/target/
|
|
||||||
src-tauri/gen/
|
|
||||||
16
.prettierrc
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 80,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"jsxSingleQuote": false,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"bracketSameLine": false,
|
|
||||||
"arrowParens": "always",
|
|
||||||
"proseWrap": "preserve",
|
|
||||||
"htmlWhitespaceSensitivity": "css",
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"embeddedLanguageFormatting": "auto"
|
|
||||||
}
|
|
||||||
@@ -34,27 +34,19 @@ npm install pnpm -g
|
|||||||
|
|
||||||
### Install Dependencies
|
### Install Dependencies
|
||||||
|
|
||||||
Install node packages
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Install apt packages ONLY for Ubuntu
|
|
||||||
|
|
||||||
```shell
|
|
||||||
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Download the Mihomo Core Binary
|
### Download the Mihomo Core Binary
|
||||||
|
|
||||||
You have two options for downloading the clash binary:
|
You have two options for downloading the clash binary:
|
||||||
|
|
||||||
- Automatically download it via the provided script:
|
- Automatically download it via the provided script:
|
||||||
```shell
|
```shell
|
||||||
pnpm run prebuild
|
pnpm run check
|
||||||
# Use '--force' to force update to the latest version
|
# Use '--force' to force update to the latest version
|
||||||
# pnpm run prebuild --force
|
# 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 [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).
|
||||||
|
|
||||||
@@ -104,29 +96,6 @@ pnpm portable
|
|||||||
|
|
||||||
## Contributing Your Changes
|
## Contributing Your Changes
|
||||||
|
|
||||||
#### Before commit your changes
|
|
||||||
|
|
||||||
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
|
|
||||||
|
|
||||||
1. Code quailty checks
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For rust backend
|
|
||||||
$ clash-verge-rev: pnpm clippy
|
|
||||||
# For frontend (not yet).
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Code style formatting
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For rust backend
|
|
||||||
$ clash-verge-rev: cd src-tauri
|
|
||||||
$ clash-verge-rev/src-tauri: cargo fmt
|
|
||||||
# For frontend
|
|
||||||
$ clash-verge-rev: pnpm format:check
|
|
||||||
$ clash-verge-rev: pnpm format
|
|
||||||
```
|
|
||||||
|
|
||||||
Once you have made your changes:
|
Once you have made your changes:
|
||||||
|
|
||||||
1. Fork the repository.
|
1. Fork the repository.
|
||||||
|
|||||||
53
README.md
@@ -1,7 +1,7 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="./src-tauri/icons/icon.png" alt="Clash" width="128" />
|
<img src="./src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||||
<br>
|
<br>
|
||||||
Fork of <a href="https://github.com/clash-verge-rev/clash-verge-rev">Clash Verge Rev</a>
|
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
||||||
<br>
|
<br>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -11,26 +11,53 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
|||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||
| Dark | Light |
|
| Dark | Light |
|
||||||
| ----------------------------------- | ------------------------------------ |
|
| -------------------------------- | --------------------------------- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Go to the [Release page](https://github.com/coolcoala/clash-verge-rev-lite/releases) to download the corresponding installation package<br>
|
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
||||||
|
Go to the [release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
||||||
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||||
|
|
||||||
### Telegram channel: ---
|
### 安装说明和常见问题,请到[文档页](https://clash-verge-rev.github.io/)查看:[Doc](https://clash-verge-rev.github.io/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TG 频道: [@clash_verge_rev](https://t.me/clash_verge_re)
|
||||||
|
|
||||||
|
## Promotion
|
||||||
|
|
||||||
|
[狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
|
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
||||||
|
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
||||||
|
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
||||||
|
- 海外团队,无跑路风险,高达 50% 返佣
|
||||||
|
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
|
||||||
|
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
|
||||||
|
- 解锁流媒体及 ChatGPT
|
||||||
|
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on the powerful Rust and Tauri 2 frameworks.
|
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
||||||
- Built-in [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) kernel with support for switching between `Alpha` versions of the kernel.
|
- 内置[Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo)内核,并支持切换 `Alpha` 版本内核。
|
||||||
- Simple and beautiful user interface with support for custom theme colors, agent group/tray icons, and `CSS Injection`.
|
- 简洁美观的用户界面,支持自定义主题颜色、代理组/托盘图标以及 `CSS Injection`。
|
||||||
- Configuration file management, configuration file syntax hints.
|
- 配置文件管理和增强(Merge 和 Script),配置文件语法提示。
|
||||||
- System Agent and Guard, `TUN (Virtual NIC)` mode.
|
- 系统代理和守卫、`TUN(虚拟网卡)` 模式。
|
||||||
- Visual node and rule editing
|
- 可视化节点和规则编辑
|
||||||
- WebDav configuration backup and synchronization
|
- WebDav 配置备份和同步
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
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
|
## Development
|
||||||
|
|
||||||
|
|||||||
1419
UPDATELOG.md
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "new-york",
|
|
||||||
"rsc": false,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "",
|
|
||||||
"css": "src/index.css",
|
|
||||||
"baseColor": "slate",
|
|
||||||
"cssVariables": true,
|
|
||||||
"prefix": ""
|
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"components": "@root/src/components",
|
|
||||||
"utils": "@root/lib/utils",
|
|
||||||
"ui": "@root/src/components/ui",
|
|
||||||
"lib": "@root/lib",
|
|
||||||
"hooks": "@root/hooks"
|
|
||||||
},
|
|
||||||
"iconLibrary": "lucide"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
files:
|
|
||||||
- source: /src/locales/en.json
|
|
||||||
translation: /src/locales
|
|
||||||
multilingual: 1
|
|
||||||
|
Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 671 KiB After Width: | Height: | Size: 162 KiB |
@@ -1,19 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768
|
|
||||||
|
|
||||||
export function useIsMobile() {
|
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
||||||
const onChange = () => {
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
||||||
}
|
|
||||||
mql.addEventListener("change", onChange)
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
||||||
return () => mql.removeEventListener("change", onChange)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return !!isMobile
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { clsx, type ClassValue } from "clsx";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs));
|
|
||||||
}
|
|
||||||
146
package.json
@@ -1,28 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "0.2.2",
|
"version": "2.1.3-alpha",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
"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",
|
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
||||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
|
"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",
|
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"web:dev": "vite",
|
"web:dev": "vite",
|
||||||
"web:build": "tsc --noEmit && vite build",
|
"web:build": "tsc --noEmit && vite build",
|
||||||
"web:serve": "vite preview",
|
"web:serve": "vite preview",
|
||||||
"prebuild": "node scripts/prebuild.mjs",
|
"check": "node scripts/check.mjs",
|
||||||
"updater": "node scripts/updater.mjs",
|
"updater": "node scripts/updater.mjs",
|
||||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||||
"portable": "node scripts/portable.mjs",
|
"portable": "node scripts/portable.mjs",
|
||||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||||
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
"fix-alpha-version": "node scripts/alpha_version.mjs",
|
||||||
"release-version": "node scripts/release-version.mjs",
|
"prepare": "husky",
|
||||||
"publish-version": "node scripts/publish-version.mjs",
|
"clean": "cd ./src-tauri && cargo clean && cd -"
|
||||||
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
|
||||||
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"format:check": "prettier --check ."
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -30,108 +26,74 @@
|
|||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"@mui/icons-material": "^7.1.1",
|
"@mui/icons-material": "^6.4.2",
|
||||||
"@mui/lab": "7.0.0-beta.13",
|
"@mui/lab": "6.0.0-beta.25",
|
||||||
"@mui/material": "^7.1.1",
|
"@mui/material": "^6.4.2",
|
||||||
"@mui/x-data-grid": "^8.5.1",
|
"@mui/x-data-grid": "^7.25.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
"@tauri-apps/api": "2.2.0",
|
||||||
"@radix-ui/react-context-menu": "^2.2.15",
|
"@tauri-apps/plugin-clipboard-manager": "^2.2.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@tauri-apps/plugin-notification": "^2.2.1",
|
||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@tauri-apps/plugin-process": "^2.2.0",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@tauri-apps/plugin-shell": "2.2.0",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@tauri-apps/plugin-updater": "2.3.0",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
|
||||||
"@tanstack/react-table": "^8.21.3",
|
|
||||||
"@tauri-apps/api": "2.5.0",
|
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
|
||||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
|
|
||||||
"@tauri-apps/plugin-notification": "^2.2.2",
|
|
||||||
"@tauri-apps/plugin-process": "^2.2.1",
|
|
||||||
"@tauri-apps/plugin-shell": "2.2.1",
|
|
||||||
"@tauri-apps/plugin-updater": "2.7.1",
|
|
||||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
|
||||||
"@types/d3-shape": "^3.1.7",
|
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"ahooks": "^3.8.5",
|
"ahooks": "^3.8.4",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.7.9",
|
||||||
"chart.js": "^4.4.9",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"cli-color": "^2.0.4",
|
"cli-color": "^2.0.4",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cmdk": "^1.1.1",
|
|
||||||
"d3-shape": "^3.2.0",
|
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"foxact": "^0.2.45",
|
"foxact": "^0.2.43",
|
||||||
"glob": "^11.0.2",
|
"glob": "^11.0.1",
|
||||||
"i18next": "^25.2.1",
|
"i18next": "^24.2.2",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.7",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.514.0",
|
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"monaco-yaml": "^5.4.0",
|
"monaco-yaml": "^5.2.3",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.0.9",
|
||||||
"next-themes": "^0.4.6",
|
"peggy": "^4.2.0",
|
||||||
"peggy": "^5.0.3",
|
"react": "^18.3.1",
|
||||||
"react": "19.1.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-error-boundary": "^4.0.12",
|
||||||
"react-colorful": "^5.6.1",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-dom": "19.1.0",
|
"react-i18next": "^13.5.0",
|
||||||
"react-error-boundary": "6.0.0",
|
"react-markdown": "^9.0.1",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-monaco-editor": "^0.56.0",
|
||||||
"react-i18next": "15.5.2",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-markdown": "10.1.0",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-monaco-editor": "0.58.0",
|
"react-virtuoso": "^4.6.3",
|
||||||
"react-router-dom": "7.6.2",
|
|
||||||
"react-virtuoso": "^4.12.8",
|
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"sonner": "^2.0.5",
|
"swr": "^2.3.0",
|
||||||
"swr": "^2.3.3",
|
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
"zod": "^3.25.67",
|
"zustand": "^5.0.3"
|
||||||
"zustand": "^5.0.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.0",
|
||||||
"@tauri-apps/cli": "2.5.0",
|
"@tauri-apps/cli": "2.2.7",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.0.0",
|
"@types/react": "^18.3.18",
|
||||||
"@types/react": "19.1.6",
|
"@types/react-dom": "^18.3.5",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"@vitejs/plugin-legacy": "^6.1.1",
|
"@vitejs/plugin-legacy": "^6.0.0",
|
||||||
"@vitejs/plugin-react": "4.5.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"autoprefixer": "^10.4.21",
|
|
||||||
"commander": "^14.0.0",
|
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"meta-json-schema": "^1.19.10",
|
"meta-json-schema": "^1.19.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"postcss": "^8.5.4",
|
"prettier": "^3.4.2",
|
||||||
"prettier": "^3.5.3",
|
"pretty-quick": "^4.0.0",
|
||||||
"pretty-quick": "^4.2.2",
|
"sass": "^1.83.4",
|
||||||
"sass": "^1.89.1",
|
"terser": "^5.37.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"typescript": "^5.7.3",
|
||||||
"terser": "^5.41.0",
|
"vite": "^6.0.11",
|
||||||
"tw-animate-css": "^1.3.4",
|
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"vite": "^6.3.5",
|
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
"vite-plugin-svgr": "^4.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
9632
pnpm-lock.yaml
generated
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["config:recommended"],
|
|
||||||
"baseBranches": ["dev"],
|
|
||||||
"enabledManagers": ["cargo", "npm"],
|
|
||||||
"labels": ["dependencies"],
|
|
||||||
"ignorePaths": [
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/bower_components/**",
|
|
||||||
"**/vendor/**",
|
|
||||||
"**/__tests__/**",
|
|
||||||
"**/test/**",
|
|
||||||
"**/tests/**",
|
|
||||||
"**/__fixtures__/**",
|
|
||||||
"**/crate/**",
|
|
||||||
"shared/**"
|
|
||||||
],
|
|
||||||
"rangeStrategy": "bump",
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"semanticCommitType": "chore",
|
|
||||||
"matchPackageNames": ["*"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Disable node/pnpm version updates",
|
|
||||||
"matchPackageNames": ["node", "pnpm"],
|
|
||||||
"matchDepTypes": ["engines", "packageManager"],
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Group all cargo dependencies into a single PR",
|
|
||||||
"matchManagers": ["cargo"],
|
|
||||||
"groupName": "cargo dependencies"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Group all npm dependencies into a single PR",
|
|
||||||
"matchManagers": ["npm"],
|
|
||||||
"groupName": "npm dependencies"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"postUpdateOptions": ["pnpmDedupe"],
|
|
||||||
"ignoreDeps": ["serde_yaml"]
|
|
||||||
}
|
|
||||||
@@ -38,17 +38,6 @@ async function updatePackageVersion(newVersion) {
|
|||||||
const packageJson = JSON.parse(data);
|
const packageJson = JSON.parse(data);
|
||||||
// 获取键值替换
|
// 获取键值替换
|
||||||
let result = packageJson.version.replace("alpha", newVersion);
|
let result = packageJson.version.replace("alpha", newVersion);
|
||||||
// 检查当前版本号是否已经包含了 alpha- 后缀
|
|
||||||
if (!packageJson.version.includes(`alpha-`)) {
|
|
||||||
// 如果只有 alpha 而没有 alpha-,则替换为 alpha-newVersion
|
|
||||||
result = packageJson.version.replace("alpha", `alpha-${newVersion}`);
|
|
||||||
} else {
|
|
||||||
// 如果已经是 alpha-xxx 格式,则更新 xxx 部分
|
|
||||||
result = packageJson.version.replace(
|
|
||||||
/alpha-[^-]*/,
|
|
||||||
`alpha-${newVersion}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
console.log("[INFO]: Current version is: ", result);
|
console.log("[INFO]: Current version is: ", result);
|
||||||
packageJson.version = result;
|
packageJson.version = result;
|
||||||
// 写入版本号
|
// 写入版本号
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
|
|
||||||
const SRC_DIRS = [
|
|
||||||
path.resolve(__dirname, "../src"),
|
|
||||||
path.resolve(__dirname, "../src-tauri"),
|
|
||||||
];
|
|
||||||
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
|
|
||||||
|
|
||||||
// 递归获取所有文件
|
|
||||||
function getAllFiles(dir, exts) {
|
|
||||||
let files = [];
|
|
||||||
fs.readdirSync(dir).forEach((file) => {
|
|
||||||
const full = path.join(dir, file);
|
|
||||||
if (fs.statSync(full).isDirectory()) {
|
|
||||||
files = files.concat(getAllFiles(full, exts));
|
|
||||||
} else if (exts.includes(path.extname(full))) {
|
|
||||||
files.push(full);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取所有源码内容为一个大字符串
|
|
||||||
function getAllSourceContent() {
|
|
||||||
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
|
|
||||||
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 白名单 key,不检查这些 key 是否被使用
|
|
||||||
const WHITELIST_KEYS = [
|
|
||||||
"theme.light",
|
|
||||||
"theme.dark",
|
|
||||||
"theme.system",
|
|
||||||
"Already Using Latest Core Version",
|
|
||||||
];
|
|
||||||
|
|
||||||
// 主流程
|
|
||||||
function processI18nFile(i18nPath, lang, allSource) {
|
|
||||||
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
|
|
||||||
const keys = Object.keys(i18n);
|
|
||||||
|
|
||||||
const used = {};
|
|
||||||
const unused = [];
|
|
||||||
|
|
||||||
let checked = 0;
|
|
||||||
const total = keys.length;
|
|
||||||
keys.forEach((key) => {
|
|
||||||
if (WHITELIST_KEYS.includes(key)) {
|
|
||||||
used[key] = i18n[key];
|
|
||||||
} else {
|
|
||||||
// 只查找一次
|
|
||||||
const regex = new RegExp(`["'\`]${key}["'\`]`);
|
|
||||||
if (regex.test(allSource)) {
|
|
||||||
used[key] = i18n[key];
|
|
||||||
} else {
|
|
||||||
unused.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checked++;
|
|
||||||
if (checked % 20 === 0 || checked === total) {
|
|
||||||
const percent = ((checked / total) * 100).toFixed(1);
|
|
||||||
process.stdout.write(
|
|
||||||
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
|
|
||||||
);
|
|
||||||
if (checked === total) process.stdout.write("\n");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 输出未使用的 key
|
|
||||||
console.log(`\n[${lang}] Unused keys:`, unused);
|
|
||||||
|
|
||||||
// 备份原文件
|
|
||||||
const oldPath = i18nPath + ".old";
|
|
||||||
fs.renameSync(i18nPath, oldPath);
|
|
||||||
|
|
||||||
// 写入精简后的 i18n 文件(保留原文件名)
|
|
||||||
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
|
|
||||||
console.log(
|
|
||||||
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
|
|
||||||
);
|
|
||||||
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
|
|
||||||
const files = fs
|
|
||||||
.readdirSync(LOCALES_DIR)
|
|
||||||
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
|
|
||||||
const allSource = getAllSourceContent();
|
|
||||||
files.forEach((file) => {
|
|
||||||
const lang = path.basename(file, ".json");
|
|
||||||
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -49,9 +49,9 @@ async function resolvePortable() {
|
|||||||
zip.addLocalFolder(
|
zip.addLocalFolder(
|
||||||
path.join(
|
path.join(
|
||||||
releaseDir,
|
releaseDir,
|
||||||
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||||
),
|
),
|
||||||
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||||
);
|
);
|
||||||
zip.addLocalFolder(configDir, ".config");
|
zip.addLocalFolder(configDir, ".config");
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
// scripts/publish-version.mjs
|
|
||||||
import { spawn } from "child_process";
|
|
||||||
import { existsSync } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const rootDir = process.cwd();
|
|
||||||
const scriptPath = path.join(rootDir, "scripts", "release-version.mjs");
|
|
||||||
|
|
||||||
if (!existsSync(scriptPath)) {
|
|
||||||
console.error("release-version.mjs not found!");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionArg = process.argv[2];
|
|
||||||
if (!versionArg) {
|
|
||||||
console.error("Usage: pnpm publish-version <version>");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 调用 release-version.mjs
|
|
||||||
const runRelease = () =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const child = spawn("node", [scriptPath, versionArg], { stdio: "inherit" });
|
|
||||||
child.on("exit", (code) => {
|
|
||||||
if (code === 0) resolve();
|
|
||||||
else reject(new Error("release-version failed"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. 判断是否需要打 tag
|
|
||||||
function isSemver(version) {
|
|
||||||
return /^v?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
await runRelease();
|
|
||||||
|
|
||||||
let tag = null;
|
|
||||||
if (versionArg === "alpha") {
|
|
||||||
// 读取 package.json 里的主版本
|
|
||||||
const pkg = await import(path.join(rootDir, "package.json"), {
|
|
||||||
assert: { type: "json" },
|
|
||||||
});
|
|
||||||
tag = `v${pkg.default.version}-alpha`;
|
|
||||||
} else if (isSemver(versionArg)) {
|
|
||||||
// 1.2.3 或 v1.2.3
|
|
||||||
tag = versionArg.startsWith("v") ? versionArg : `v${versionArg}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tag) {
|
|
||||||
// 打 tag 并推送
|
|
||||||
const { execSync } = await import("child_process");
|
|
||||||
try {
|
|
||||||
execSync(`git tag ${tag}`, { stdio: "inherit" });
|
|
||||||
execSync(`git push origin ${tag}`, { stdio: "inherit" });
|
|
||||||
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("[INFO]: No git tag created for this version.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
/**
|
|
||||||
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* pnpm release-version <version>
|
|
||||||
*
|
|
||||||
* <version> can be:
|
|
||||||
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
|
|
||||||
* - A tag: "alpha", "beta", "rc", or "autobuild"
|
|
||||||
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
|
||||||
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
* pnpm release-version 1.2.3
|
|
||||||
* pnpm release-version v1.2.3-beta
|
|
||||||
* pnpm release-version beta
|
|
||||||
* pnpm release-version autobuild
|
|
||||||
*
|
|
||||||
* The script will:
|
|
||||||
* - Validate and normalize the version argument
|
|
||||||
* - Update the version field in package.json
|
|
||||||
* - Update the version field in src-tauri/Cargo.toml
|
|
||||||
* - Update the version field in src-tauri/tauri.conf.json
|
|
||||||
*
|
|
||||||
* Errors are logged and the process exits with code 1 on failure.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fs from "fs/promises";
|
|
||||||
import path from "path";
|
|
||||||
import { program } from "commander";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前 git 短 commit hash
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getGitShortCommit() {
|
|
||||||
try {
|
|
||||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
|
|
||||||
return "nogit";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
|
|
||||||
* @param {boolean} withCommit 是否带 commit
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function generateShortTimestamp(withCommit = false) {
|
|
||||||
const now = new Date();
|
|
||||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(now.getDate()).padStart(2, "0");
|
|
||||||
if (withCommit) {
|
|
||||||
const gitShort = getGitShortCommit();
|
|
||||||
return `${month}${day}.${gitShort}`;
|
|
||||||
}
|
|
||||||
return `${month}${day}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证版本号格式
|
|
||||||
* @param {string} version
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function isValidVersion(version) {
|
|
||||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
|
||||||
version,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标准化版本号
|
|
||||||
* @param {string} version
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function normalizeVersion(version) {
|
|
||||||
return version.startsWith("v") ? version : `v${version}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取基础版本号(去掉所有 -tag 和 +build 部分)
|
|
||||||
* @param {string} version
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getBaseVersion(version) {
|
|
||||||
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
|
|
||||||
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新 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}`,
|
|
||||||
);
|
|
||||||
} 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 baseVersion = getBaseVersion(versionWithoutV);
|
|
||||||
|
|
||||||
const updatedLines = lines.map((line) => {
|
|
||||||
if (line.trim().startsWith("version =")) {
|
|
||||||
return line.replace(
|
|
||||||
/version\s*=\s*"[^"]+"/,
|
|
||||||
`version = "${baseVersion}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
|
||||||
console.log(`[INFO]: Cargo.toml version updated to: ${baseVersion}`);
|
|
||||||
} 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;
|
|
||||||
const baseVersion = getBaseVersion(versionWithoutV);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[INFO]: Current tauri.conf.json version is: ",
|
|
||||||
tauriConfig.version,
|
|
||||||
);
|
|
||||||
tauriConfig.version = baseVersion;
|
|
||||||
await fs.writeFile(
|
|
||||||
tauriConfigPath,
|
|
||||||
JSON.stringify(tauriConfig, null, 2),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating tauri.conf.json version:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前版本号
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主函数
|
|
||||||
*/
|
|
||||||
async function main(versionArg) {
|
|
||||||
if (!versionArg) {
|
|
||||||
console.error("Error: Version argument is required");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let newVersion;
|
|
||||||
const validTags = ["alpha", "beta", "rc", "autobuild"];
|
|
||||||
|
|
||||||
if (validTags.includes(versionArg.toLowerCase())) {
|
|
||||||
const currentVersion = await getCurrentVersion();
|
|
||||||
const baseVersion = getBaseVersion(currentVersion);
|
|
||||||
|
|
||||||
if (versionArg.toLowerCase() === "autobuild") {
|
|
||||||
// 格式: 2.3.0+autobuild.250613.cc39b27
|
|
||||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
|
|
||||||
} else {
|
|
||||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!isValidVersion(versionArg)) {
|
|
||||||
console.error("Error: Invalid version format");
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
program
|
|
||||||
.name("pnpm release-version")
|
|
||||||
.description("Update project version numbers")
|
|
||||||
.argument("<version>", "version tag or full version")
|
|
||||||
.action(main)
|
|
||||||
.parse(process.argv);
|
|
||||||
@@ -43,42 +43,3 @@ export async function resolveUpdateLog(tag) {
|
|||||||
|
|
||||||
return map[tag].join("\n").trim();
|
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,6 +1,6 @@
|
|||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from "@actions/github";
|
||||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||||
|
|
||||||
// Add stable update JSON filenames
|
// Add stable update JSON filenames
|
||||||
const UPDATE_TAG_NAME = "updater";
|
const UPDATE_TAG_NAME = "updater";
|
||||||
@@ -8,8 +8,8 @@ const UPDATE_JSON_FILE = "update.json";
|
|||||||
const UPDATE_JSON_PROXY = "update-proxy.json";
|
const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
// Add alpha update JSON filenames
|
// Add alpha update JSON filenames
|
||||||
const ALPHA_TAG_NAME = "updater-alpha";
|
const ALPHA_TAG_NAME = "updater-alpha";
|
||||||
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
const ALPHA_UPDATE_JSON_FILE = "update-alpha.json";
|
||||||
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
const ALPHA_UPDATE_JSON_PROXY = "update-alpha-proxy.json";
|
||||||
|
|
||||||
/// generate update.json
|
/// generate update.json
|
||||||
/// upload to update tag's release asset
|
/// upload to update tag's release asset
|
||||||
@@ -78,235 +78,192 @@ async function resolveUpdater() {
|
|||||||
async function processRelease(github, options, tag, isAlpha) {
|
async function processRelease(github, options, tag, isAlpha) {
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
|
|
||||||
try {
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
...options,
|
||||||
...options,
|
tag: tag.name,
|
||||||
tag: tag.name,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
notes: await resolveUpdateLog(tag.name).catch(() =>
|
notes: await resolveUpdateLog(tag.name).catch(
|
||||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
() => "No changelog available",
|
||||||
),
|
),
|
||||||
pub_date: new Date().toISOString(),
|
pub_date: new Date().toISOString(),
|
||||||
platforms: {
|
platforms: {
|
||||||
win64: { signature: "", url: "" }, // compatible with older formats
|
win64: { signature: "", url: "" }, // compatible with older formats
|
||||||
linux: { signature: "", url: "" }, // compatible with older formats
|
linux: { signature: "", url: "" }, // compatible with older formats
|
||||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||||
"darwin-aarch64": { signature: "", url: "" },
|
"darwin-aarch64": { signature: "", url: "" },
|
||||||
"darwin-intel": { signature: "", url: "" },
|
"darwin-intel": { signature: "", url: "" },
|
||||||
"darwin-x86_64": { signature: "", url: "" },
|
"darwin-x86_64": { signature: "", url: "" },
|
||||||
"linux-x86_64": { signature: "", url: "" },
|
"linux-x86_64": { signature: "", url: "" },
|
||||||
"linux-x86": { signature: "", url: "" },
|
"linux-x86": { signature: "", url: "" },
|
||||||
"linux-i686": { signature: "", url: "" },
|
"linux-i686": { signature: "", url: "" },
|
||||||
"linux-aarch64": { signature: "", url: "" },
|
"linux-aarch64": { signature: "", url: "" },
|
||||||
"linux-armv7": { signature: "", url: "" },
|
"linux-armv7": { signature: "", url: "" },
|
||||||
"windows-x86_64": { signature: "", url: "" },
|
"windows-x86_64": { signature: "", url: "" },
|
||||||
"windows-aarch64": { signature: "", url: "" },
|
"windows-aarch64": { signature: "", url: "" },
|
||||||
"windows-x86": { signature: "", url: "" },
|
"windows-x86": { signature: "", url: "" },
|
||||||
"windows-i686": { signature: "", url: "" },
|
"windows-i686": { signature: "", url: "" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const promises = release.assets.map(async (asset) => {
|
const promises = release.assets.map(async (asset) => {
|
||||||
const { name, browser_download_url } = asset;
|
const { name, browser_download_url } = asset;
|
||||||
|
|
||||||
// Process all the platform URL and signature data
|
// Process all the platform URL and signature data
|
||||||
// win64 url
|
// win64 url
|
||||||
if (name.endsWith("x64-setup.exe")) {
|
if (name.endsWith("x64-setup.exe")) {
|
||||||
updateData.platforms.win64.url = browser_download_url;
|
updateData.platforms.win64.url = browser_download_url;
|
||||||
updateData.platforms["windows-x86_64"].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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
// win64 signature
|
||||||
if (error.status === 404) {
|
if (name.endsWith("x64-setup.exe.sig")) {
|
||||||
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
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 {
|
} else {
|
||||||
console.error(
|
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||||
`Failed to get release for tag: ${tag.name}`,
|
|
||||||
error.message,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
tag: releaseTag,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
avoid-breaking-exported-api = true
|
|
||||||
3178
src-tauri/Cargo.lock
generated
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "0.2.2"
|
version = "2.1.2"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
|
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/coolcoala/clash-verge-rev-lite.git"
|
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||||
default-run = "clash-verge"
|
default-run = "clash-verge"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
@@ -13,104 +13,80 @@ build = "build.rs"
|
|||||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.3.0", features = [] }
|
tauri-build = { version = "2.0.6", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
url = "2.5.4"
|
warp = "0.3"
|
||||||
os_info = "3.0"
|
anyhow = "1.0.97"
|
||||||
machine-uid = "0.2"
|
|
||||||
warp = "0.3.7"
|
|
||||||
anyhow = "1.0.98"
|
|
||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
open = "5.3.2"
|
open = "5.1"
|
||||||
log = "0.4.27"
|
log = "0.4"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0"
|
||||||
log4rs = "1.3.0"
|
log4rs = "1"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.40"
|
||||||
sysinfo = "0.35.2"
|
sysinfo = "0.33.1"
|
||||||
boa_engine = "0.20.0"
|
boa_engine = "0.20.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9.34-deprecated"
|
serde_yaml = "0.9"
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.20.3"
|
||||||
lazy_static = "1.5.0"
|
|
||||||
port_scanner = "0.1.5"
|
port_scanner = "0.1.5"
|
||||||
delay_timer = "0.11.6"
|
delay_timer = "0.11.6"
|
||||||
parking_lot = "0.12.4"
|
parking_lot = "0.12"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
tokio = { version = "1.45.1", features = [
|
window-shadows = { version = "0.2.2" }
|
||||||
"rt-multi-thread",
|
tokio = { version = "1.43", features = ["full"] }
|
||||||
"macros",
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
"time",
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||||
"sync",
|
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
||||||
] }
|
image = "0.25.5"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
|
||||||
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] }
|
|
||||||
regex = "1.11.1"
|
|
||||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
|
||||||
image = "0.25.6"
|
|
||||||
imageproc = "0.25.0"
|
imageproc = "0.25.0"
|
||||||
tauri = { version = "2.6.2", features = [
|
rusttype = "0.9"
|
||||||
"protocol-asset",
|
tauri = { version = "2.3.1", features = [
|
||||||
"devtools",
|
"protocol-asset",
|
||||||
"tray-icon",
|
"devtools",
|
||||||
"image-ico",
|
"tray-icon",
|
||||||
"image-png",
|
"image-ico",
|
||||||
|
"image-png",
|
||||||
] }
|
] }
|
||||||
network-interface = { version = "2.0.1", features = ["serde"] }
|
network-interface = { version = "2.0.0", features = ["serde"] }
|
||||||
tauri-plugin-shell = "2.3.0"
|
tauri-plugin-shell = "2.2.0"
|
||||||
tauri-plugin-dialog = "2.3.0"
|
tauri-plugin-dialog = "2.2.0"
|
||||||
tauri-plugin-fs = "2.4.0"
|
tauri-plugin-fs = "2.2.0"
|
||||||
tauri-plugin-process = "2.3.0"
|
tauri-plugin-notification = "2.2.1"
|
||||||
tauri-plugin-clipboard-manager = "2.3.0"
|
tauri-plugin-process = "2.2.0"
|
||||||
tauri-plugin-deep-link = "2.4.0"
|
tauri-plugin-clipboard-manager = "2.2.1"
|
||||||
tauri-plugin-devtools = "2.0.0"
|
tauri-plugin-deep-link = "2.2.0"
|
||||||
tauri-plugin-window-state = "2.3.0"
|
tauri-plugin-devtools = "2.0.0-rc"
|
||||||
zip = "4.2.0"
|
url = "2.5.4"
|
||||||
reqwest_dav = "0.2.1"
|
zip = "2.2.3"
|
||||||
|
reqwest_dav = "0.1.14"
|
||||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
getrandom = "0.3.3"
|
getrandom = "0.3.1"
|
||||||
tokio-tungstenite = "0.27.0"
|
tokio-tungstenite = "0.26.2"
|
||||||
futures = "0.3.31"
|
futures = "0.3"
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.1"
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.87"
|
||||||
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
mihomo_api = { path = "./src/crate_mihomo_api" }
|
||||||
ab_glyph = "0.2.29"
|
ab_glyph = "0.2.29"
|
||||||
tungstenite = "0.27.0"
|
tungstenite = "0.26.2"
|
||||||
libc = "0.2.174"
|
|
||||||
gethostname = "1.0.2"
|
|
||||||
hmac = "0.12.1"
|
|
||||||
sha2 = "0.10.9"
|
|
||||||
hex = "0.4.3"
|
|
||||||
scopeguard = "1.2.0"
|
|
||||||
tauri-plugin-notification = "2.3.0"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
deelevate = "0.2.0"
|
deelevate = "0.2.0"
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
winapi = { version = "0.3.9", features = [
|
url = "2.5.4"
|
||||||
"winbase",
|
|
||||||
"fileapi",
|
|
||||||
"winnt",
|
|
||||||
"handleapi",
|
|
||||||
"errhandlingapi",
|
|
||||||
"minwindef",
|
|
||||||
"winerror",
|
|
||||||
"tlhelp32",
|
|
||||||
"processthreadsapi",
|
|
||||||
"winhttp",
|
|
||||||
"winreg",
|
|
||||||
] }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
users = "0.11.0"
|
users = "0.11.0"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-autostart = "2.5.0"
|
tauri-plugin-autostart = "2.2.0"
|
||||||
tauri-plugin-global-shortcut = "2.3.0"
|
tauri-plugin-global-shortcut = "2.2.0"
|
||||||
tauri-plugin-updater = "2.9.0"
|
tauri-plugin-updater = "2.5.1"
|
||||||
|
tauri-plugin-window-state = "2.2.1"
|
||||||
|
#openssl
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["custom-protocol"]
|
default = ["custom-protocol"]
|
||||||
@@ -126,31 +102,34 @@ strip = true
|
|||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
incremental = true
|
incremental = true
|
||||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
|
||||||
opt-level = 0 # 禁用优化,进一步提升编译速度
|
|
||||||
debug = true # 保留调试信息
|
|
||||||
strip = false # 不剥离符号,保留调试信息
|
|
||||||
|
|
||||||
[profile.fast-release]
|
[profile.fast-release]
|
||||||
inherits = "release" # 继承 release 的配置
|
inherits = "release" # 继承 release 的配置
|
||||||
panic = "abort" # 与 release 相同
|
panic = "abort" # 与 release 相同
|
||||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||||
lto = false # 禁用 LTO,提升编译速度
|
lto = false # 禁用 LTO,提升编译速度
|
||||||
opt-level = 0 # 禁用优化,大幅提升编译速度
|
opt-level = 0 # 禁用优化,大幅提升编译速度
|
||||||
debug = true # 保留调试信息
|
debug = true # 保留调试信息
|
||||||
strip = false # 不剥离符号,保留调试信息
|
strip = false # 不剥离符号,保留调试信息
|
||||||
|
|
||||||
|
[profile.fast-dev]
|
||||||
|
inherits = "dev" # 继承 dev 的配置
|
||||||
|
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||||
|
opt-level = 0 # 禁用优化,进一步提升编译速度
|
||||||
|
incremental = true # 启用增量编译
|
||||||
|
debug = true # 保留调试信息
|
||||||
|
strip = false # 不剥离符号,保留调试信息
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "app_lib"
|
name = "app_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.20.0"
|
env_logger = "0.11.0"
|
||||||
|
mockito = "1.7.0"
|
||||||
|
tempfile = "3.17.1"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["src_crates/crate_mihomo_api"]
|
members = [
|
||||||
|
"src/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" }
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"identifier": "desktop-windows-capability",
|
|
||||||
"description": "permissions for desktop windows applications",
|
|
||||||
"windows": ["main"],
|
|
||||||
"permissions": [
|
|
||||||
"core:webview:allow-create-webview",
|
|
||||||
"core:webview:allow-create-webview-window"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -14,10 +14,8 @@
|
|||||||
"updater:allow-download-and-install",
|
"updater:allow-download-and-install",
|
||||||
"process:allow-restart",
|
"process:allow-restart",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"autostart:allow-enable",
|
"window-state:default",
|
||||||
"autostart:allow-disable",
|
"window-state:default",
|
||||||
"autostart:allow-is-enabled",
|
"autostart:default"
|
||||||
"core:window:allow-set-theme",
|
|
||||||
"notification:default"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"shell:allow-spawn",
|
"shell:allow-spawn",
|
||||||
"shell:allow-stdin-write",
|
"shell:allow-stdin-write",
|
||||||
"dialog:allow-open",
|
"dialog:allow-open",
|
||||||
|
"notification:default",
|
||||||
"global-shortcut:allow-is-registered",
|
"global-shortcut:allow-is-registered",
|
||||||
"global-shortcut:allow-register",
|
"global-shortcut:allow-register",
|
||||||
"global-shortcut:allow-register-all",
|
"global-shortcut:allow-register-all",
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
"clipboard-manager:allow-read-text",
|
"clipboard-manager:allow-read-text",
|
||||||
"clipboard-manager:allow-write-text",
|
"clipboard-manager:allow-write-text",
|
||||||
"shell:default",
|
"shell:default",
|
||||||
"dialog:default"
|
"dialog:default",
|
||||||
|
"notification:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
# This template contains all of the possible sections and their default values
|
|
||||||
|
|
||||||
# Note that all fields that take a lint level have these possible values:
|
|
||||||
# * deny - An error will be produced and the check will fail
|
|
||||||
# * warn - A warning will be produced, but the check will not fail
|
|
||||||
# * allow - No warning or error will be produced, though in some cases a note
|
|
||||||
# will be
|
|
||||||
|
|
||||||
# The values provided in this template are the default values that will be used
|
|
||||||
# when any section or field is not specified in your own configuration
|
|
||||||
|
|
||||||
# Root options
|
|
||||||
|
|
||||||
# The graph table configures how the dependency graph is constructed and thus
|
|
||||||
# which crates the checks are performed against
|
|
||||||
[graph]
|
|
||||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
|
||||||
# only the specified targets will be checked when running `cargo deny check`.
|
|
||||||
# This means, if a particular package is only ever used as a target specific
|
|
||||||
# dependency, such as, for example, the `nix` crate only being used via the
|
|
||||||
# `target_family = "unix"` configuration, that only having windows targets in
|
|
||||||
# this list would mean the nix crate, as well as any of its exclusive
|
|
||||||
# dependencies not shared by any other crates, would be ignored, as the target
|
|
||||||
# list here is effectively saying which targets you are building for.
|
|
||||||
targets = [
|
|
||||||
# The triple can be any string, but only the target triples built in to
|
|
||||||
# rustc (as of 1.40) can be checked against actual config expressions
|
|
||||||
#"x86_64-unknown-linux-musl",
|
|
||||||
# You can also specify which target_features you promise are enabled for a
|
|
||||||
# particular target. target_features are currently not validated against
|
|
||||||
# the actual valid features supported by the target architecture.
|
|
||||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
|
||||||
]
|
|
||||||
# When creating the dependency graph used as the source of truth when checks are
|
|
||||||
# executed, this field can be used to prune crates from the graph, removing them
|
|
||||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
|
||||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
|
||||||
# they are connected to another crate in the graph that hasn't been pruned,
|
|
||||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
|
||||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
|
||||||
#exclude = []
|
|
||||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
|
||||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
|
||||||
# is recommended to pass `--all-features` on the cmd line instead
|
|
||||||
all-features = false
|
|
||||||
# If true, metadata will be collected with `--no-default-features`. The same
|
|
||||||
# caveat with `all-features` applies
|
|
||||||
no-default-features = false
|
|
||||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
|
||||||
# is specified on the cmd line they will take precedence over this option.
|
|
||||||
#features = []
|
|
||||||
|
|
||||||
# The output table provides options for how/if diagnostics are outputted
|
|
||||||
[output]
|
|
||||||
# When outputting inclusion graphs in diagnostics that include features, this
|
|
||||||
# option can be used to specify the depth at which feature edges will be added.
|
|
||||||
# This option is included since the graphs can be quite large and the addition
|
|
||||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
|
||||||
# This option can be overridden via `--feature-depth` on the cmd line
|
|
||||||
feature-depth = 1
|
|
||||||
|
|
||||||
# This section is considered when running `cargo deny check advisories`
|
|
||||||
# More documentation for the advisories section can be found here:
|
|
||||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
|
||||||
[advisories]
|
|
||||||
# The path where the advisory databases are cloned/fetched into
|
|
||||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
|
||||||
# The url(s) of the advisory databases to use
|
|
||||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
|
||||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
|
||||||
# output a note when they are encountered.
|
|
||||||
ignore = [
|
|
||||||
#"RUSTSEC-0000-0000",
|
|
||||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
|
||||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
|
||||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
|
||||||
"RUSTSEC-2024-0415",
|
|
||||||
]
|
|
||||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
|
||||||
# If this is false, then it uses a built-in git library.
|
|
||||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
|
||||||
# See Git Authentication for more information about setting up git authentication.
|
|
||||||
#git-fetch-with-cli = true
|
|
||||||
|
|
||||||
# This section is considered when running `cargo deny check licenses`
|
|
||||||
# More documentation for the licenses section can be found here:
|
|
||||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
|
||||||
[licenses]
|
|
||||||
# List of explicitly allowed licenses
|
|
||||||
# See https://spdx.org/licenses/ for list of possible licenses
|
|
||||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
|
||||||
allow = [
|
|
||||||
#"MIT",
|
|
||||||
#"Apache-2.0",
|
|
||||||
#"Apache-2.0 WITH LLVM-exception",
|
|
||||||
]
|
|
||||||
# The confidence threshold for detecting a license from license text.
|
|
||||||
# The higher the value, the more closely the license text must be to the
|
|
||||||
# canonical license text of a valid SPDX license file.
|
|
||||||
# [possible values: any between 0.0 and 1.0].
|
|
||||||
confidence-threshold = 0.85
|
|
||||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
|
||||||
# aren't accepted for every possible crate as with the normal allow list
|
|
||||||
exceptions = [
|
|
||||||
# Each entry is the crate and version constraint, and its specific allow
|
|
||||||
# list
|
|
||||||
#{ allow = ["Zlib"], crate = "adler32" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# Some crates don't have (easily) machine readable licensing information,
|
|
||||||
# adding a clarification entry for it allows you to manually specify the
|
|
||||||
# licensing information
|
|
||||||
#[[licenses.clarify]]
|
|
||||||
# The package spec the clarification applies to
|
|
||||||
#crate = "ring"
|
|
||||||
# The SPDX expression for the license requirements of the crate
|
|
||||||
#expression = "MIT AND ISC AND OpenSSL"
|
|
||||||
# One or more files in the crate's source used as the "source of truth" for
|
|
||||||
# the license expression. If the contents match, the clarification will be used
|
|
||||||
# when running the license check, otherwise the clarification will be ignored
|
|
||||||
# and the crate will be checked normally, which may produce warnings or errors
|
|
||||||
# depending on the rest of your configuration
|
|
||||||
#license-files = [
|
|
||||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
|
||||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
|
||||||
#]
|
|
||||||
|
|
||||||
[licenses.private]
|
|
||||||
# If true, ignores workspace crates that aren't published, or are only
|
|
||||||
# published to private registries.
|
|
||||||
# To see how to mark a crate as unpublished (to the official registry),
|
|
||||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
|
||||||
ignore = false
|
|
||||||
# One or more private registries that you might publish crates to, if a crate
|
|
||||||
# is only published to private registries, and ignore is true, the crate will
|
|
||||||
# not have its license(s) checked
|
|
||||||
registries = [
|
|
||||||
#"https://sekretz.com/registry
|
|
||||||
]
|
|
||||||
|
|
||||||
# This section is considered when running `cargo deny check bans`.
|
|
||||||
# More documentation about the 'bans' section can be found here:
|
|
||||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
|
||||||
[bans]
|
|
||||||
# Lint level for when multiple versions of the same crate are detected
|
|
||||||
multiple-versions = "warn"
|
|
||||||
# Lint level for when a crate version requirement is `*`
|
|
||||||
wildcards = "allow"
|
|
||||||
# The graph highlighting used when creating dotgraphs for crates
|
|
||||||
# with multiple versions
|
|
||||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
|
||||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
|
||||||
# * all - Both lowest-version and simplest-path are used
|
|
||||||
highlight = "all"
|
|
||||||
# The default lint level for `default` features for crates that are members of
|
|
||||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
|
||||||
# `default` on a crate-by-crate basis if desired.
|
|
||||||
workspace-default-features = "allow"
|
|
||||||
# The default lint level for `default` features for external crates that are not
|
|
||||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
|
||||||
# on a crate-by-crate basis if desired.
|
|
||||||
external-default-features = "allow"
|
|
||||||
# List of crates that are allowed. Use with care!
|
|
||||||
allow = [
|
|
||||||
#"ansi_term@0.11.0",
|
|
||||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
|
||||||
]
|
|
||||||
# List of crates to deny
|
|
||||||
deny = [
|
|
||||||
#"ansi_term@0.11.0",
|
|
||||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
|
||||||
# Wrapper crates can optionally be specified to allow the crate when it
|
|
||||||
# is a direct dependency of the otherwise banned crate
|
|
||||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
|
||||||
]
|
|
||||||
|
|
||||||
# List of features to allow/deny
|
|
||||||
# Each entry the name of a crate and a version range. If version is
|
|
||||||
# not specified, all versions will be matched.
|
|
||||||
#[[bans.features]]
|
|
||||||
#crate = "reqwest"
|
|
||||||
# Features to not allow
|
|
||||||
#deny = ["json"]
|
|
||||||
# Features to allow
|
|
||||||
#allow = [
|
|
||||||
# "rustls",
|
|
||||||
# "__rustls",
|
|
||||||
# "__tls",
|
|
||||||
# "hyper-rustls",
|
|
||||||
# "rustls",
|
|
||||||
# "rustls-pemfile",
|
|
||||||
# "rustls-tls-webpki-roots",
|
|
||||||
# "tokio-rustls",
|
|
||||||
# "webpki-roots",
|
|
||||||
#]
|
|
||||||
# If true, the allowed features must exactly match the enabled feature set. If
|
|
||||||
# this is set there is no point setting `deny`
|
|
||||||
#exact = true
|
|
||||||
|
|
||||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
|
||||||
skip = [
|
|
||||||
#"ansi_term@0.11.0",
|
|
||||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
|
||||||
]
|
|
||||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
|
||||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
|
||||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
|
||||||
# by default infinite.
|
|
||||||
skip-tree = [
|
|
||||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
|
||||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
|
||||||
]
|
|
||||||
|
|
||||||
# This section is considered when running `cargo deny check sources`.
|
|
||||||
# More documentation about the 'sources' section can be found here:
|
|
||||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
|
||||||
[sources]
|
|
||||||
# Lint level for what to happen when a crate from a crate registry that is not
|
|
||||||
# in the allow list is encountered
|
|
||||||
unknown-registry = "warn"
|
|
||||||
# Lint level for what to happen when a crate from a git repository that is not
|
|
||||||
# in the allow list is encountered
|
|
||||||
unknown-git = "warn"
|
|
||||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
|
||||||
# if not specified. If it is specified but empty, no registries are allowed.
|
|
||||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
|
||||||
# List of URLs for allowed Git repositories
|
|
||||||
allow-git = []
|
|
||||||
|
|
||||||
[sources.allow-org]
|
|
||||||
# github.com organizations to allow git sources for
|
|
||||||
github = []
|
|
||||||
# gitlab.com organizations to allow git sources for
|
|
||||||
gitlab = []
|
|
||||||
# bitbucket.org organizations to allow git sources for
|
|
||||||
bitbucket = []
|
|
||||||
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 107 KiB |
@@ -916,10 +916,6 @@ FunctionEnd
|
|||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
Section Uninstall
|
Section Uninstall
|
||||||
;删除 window-state.json 文件
|
|
||||||
SetShellVarContext current
|
|
||||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
|
||||||
|
|
||||||
!insertmacro CheckIfAppIsRunning
|
!insertmacro CheckIfAppIsRunning
|
||||||
!insertmacro CheckAllVergeProcesses
|
!insertmacro CheckAllVergeProcesses
|
||||||
!insertmacro RemoveVergeService
|
!insertmacro RemoveVergeService
|
||||||
@@ -979,23 +975,16 @@ Section Uninstall
|
|||||||
RMDir "$INSTDIR"
|
RMDir "$INSTDIR"
|
||||||
|
|
||||||
!insertmacro DeleteAppUserModelId
|
!insertmacro DeleteAppUserModelId
|
||||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
||||||
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
|
!insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk"
|
||||||
; 兼容旧名称快捷方式
|
|
||||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
|
||||||
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
|
|
||||||
|
|
||||||
; Remove start menu shortcut
|
; Remove start menu shortcut
|
||||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
||||||
; 兼容旧名称快捷方式
|
|
||||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
|
||||||
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
||||||
|
|
||||||
; Remove desktop shortcuts
|
; Remove desktop shortcuts
|
||||||
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
|
Delete "$DESKTOP\${MAINBINARYNAME}.lnk"
|
||||||
; 兼容旧名称快捷方式
|
|
||||||
Delete "$DESKTOP\clash-verge.lnk"
|
|
||||||
|
|
||||||
; Remove registry information for add/remove programs
|
; Remove registry information for add/remove programs
|
||||||
!if "${INSTALLMODE}" == "both"
|
!if "${INSTALLMODE}" == "both"
|
||||||
@@ -1015,10 +1004,6 @@ Section Uninstall
|
|||||||
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
;删除 window-state.json 文件
|
|
||||||
SetShellVarContext current
|
|
||||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
|
||||||
|
|
||||||
${GetOptions} $CMDLINE "/P" $R0
|
${GetOptions} $CMDLINE "/P" $R0
|
||||||
IfErrors +2 0
|
IfErrors +2 0
|
||||||
SetAutoClose true
|
SetAutoClose true
|
||||||
@@ -1061,12 +1046,12 @@ FunctionEnd
|
|||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
Function CreateDesktopShortcut
|
Function CreateDesktopShortcut
|
||||||
CreateShortcut "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
CreateShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||||
!insertmacro SetLnkAppUserModelId "$DESKTOP\${PRODUCTNAME}.lnk"
|
!insertmacro SetLnkAppUserModelId "$DESKTOP\${MAINBINARYNAME}.lnk"
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
Function CreateStartMenuShortcut
|
Function CreateStartMenuShortcut
|
||||||
CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder"
|
CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder"
|
||||||
CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||||
!insertmacro SetLnkAppUserModelId "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
!insertmacro SetLnkAppUserModelId "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ merge_derives = true
|
|||||||
use_try_shorthand = false
|
use_try_shorthand = false
|
||||||
use_field_init_shorthand = false
|
use_field_init_shorthand = false
|
||||||
force_explicit_abi = true
|
force_explicit_abi = true
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::{feat, utils::dirs, wrap_err};
|
||||||
feat, logging,
|
|
||||||
utils::{dirs, logging::Type},
|
|
||||||
wrap_err,
|
|
||||||
};
|
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
/// 打开应用程序所在目录
|
/// 打开应用程序所在目录
|
||||||
@@ -49,7 +45,7 @@ pub fn open_devtools(app_handle: tauri::AppHandle) {
|
|||||||
/// 退出应用
|
/// 退出应用
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn exit_app() {
|
pub fn exit_app() {
|
||||||
feat::quit();
|
feat::quit(Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重启应用
|
/// 重启应用
|
||||||
@@ -74,46 +70,46 @@ pub fn get_app_dir() -> CmdResult<String> {
|
|||||||
Ok(app_home_dir)
|
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]
|
#[tauri::command]
|
||||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
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_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||||
let icon_path = icon_cache_dir.join(&name);
|
let icon_path = icon_cache_dir.join(&name);
|
||||||
|
|
||||||
|
// 如果文件已存在,直接返回路径
|
||||||
if icon_path.exists() {
|
if icon_path.exists() {
|
||||||
return Ok(icon_path.to_string_lossy().to_string());
|
return Ok(icon_path.to_string_lossy().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保缓存目录存在
|
||||||
if !icon_cache_dir.exists() {
|
if !icon_cache_dir.exists() {
|
||||||
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用临时文件名来下载
|
||||||
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
|
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
|
||||||
|
|
||||||
|
// 下载文件到临时位置
|
||||||
let response = wrap_err!(reqwest::get(&url).await)?;
|
let response = wrap_err!(reqwest::get(&url).await)?;
|
||||||
|
|
||||||
let content_type = response
|
// 检查内容类型是否为图片
|
||||||
.headers()
|
let content_type = response.headers()
|
||||||
.get(reqwest::header::CONTENT_TYPE)
|
.get(reqwest::header::CONTENT_TYPE)
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
let is_image = content_type.starts_with("image/");
|
let is_image = content_type.starts_with("image/");
|
||||||
|
|
||||||
|
// 获取响应内容
|
||||||
let content = wrap_err!(response.bytes().await)?;
|
let content = wrap_err!(response.bytes().await)?;
|
||||||
|
|
||||||
let is_html = content.len() > 15
|
// 检查内容是否为HTML (针对CDN错误页面)
|
||||||
&& (content.starts_with(b"<!DOCTYPE html")
|
let is_html = content.len() > 15 &&
|
||||||
|| content.starts_with(b"<html")
|
(content.starts_with(b"<!DOCTYPE html") ||
|
||||||
|| content.starts_with(b"<?xml"));
|
content.starts_with(b"<html") ||
|
||||||
|
content.starts_with(b"<?xml"));
|
||||||
|
|
||||||
|
// 只有当内容确实是图片时才保存
|
||||||
if is_image && !is_html {
|
if is_image && !is_html {
|
||||||
{
|
{
|
||||||
let mut file = match std::fs::File::create(&temp_path) {
|
let mut file = match std::fs::File::create(&temp_path) {
|
||||||
@@ -126,13 +122,14 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 再次检查目标文件是否已存在,避免重命名覆盖其他线程已完成的文件
|
||||||
if !icon_path.exists() {
|
if !icon_path.exists() {
|
||||||
match std::fs::rename(&temp_path, &icon_path) {
|
match std::fs::rename(&temp_path, &icon_path) {
|
||||||
Ok(_) => {}
|
Ok(_) => {},
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let _ = std::fs::remove_file(&temp_path);
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
if icon_path.exists() {
|
if icon_path.exists() {
|
||||||
@@ -143,11 +140,11 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
|||||||
} else {
|
} else {
|
||||||
let _ = std::fs::remove_file(&temp_path);
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(icon_path.to_string_lossy().to_string())
|
Ok(icon_path.to_string_lossy().to_string())
|
||||||
} else {
|
} else {
|
||||||
let _ = std::fs::remove_file(&temp_path);
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
Err(format!("下载的内容不是有效图片: {url}"))
|
Err(format!("下载的内容不是有效图片: {}", url).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +158,8 @@ pub struct IconInfo {
|
|||||||
/// 复制图标文件
|
/// 复制图标文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||||
use std::{fs, path::Path};
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
let file_path = Path::new(&path);
|
let file_path = Path::new(&path);
|
||||||
|
|
||||||
@@ -189,14 +187,7 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
|||||||
)
|
)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
}
|
}
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"Copying icon file path: {:?} -> file dist: {:?}",
|
|
||||||
path,
|
|
||||||
dest_path
|
|
||||||
);
|
|
||||||
match fs::copy(file_path, &dest_path) {
|
match fs::copy(file_path, &dest_path) {
|
||||||
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
@@ -205,42 +196,3 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
|||||||
Err("file not found".to_string())
|
Err("file not found".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通知UI已准备就绪
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn notify_ui_ready() -> CmdResult<()> {
|
|
||||||
log::info!(target: "app", "前端UI已准备就绪");
|
|
||||||
crate::utils::resolve::mark_ui_ready();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// UI加载阶段
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
|
||||||
log::info!(target: "app", "UI加载阶段更新: {stage}");
|
|
||||||
|
|
||||||
use crate::utils::resolve::UiReadyStage;
|
|
||||||
|
|
||||||
let stage_enum = match stage.as_str() {
|
|
||||||
"NotStarted" => UiReadyStage::NotStarted,
|
|
||||||
"Loading" => UiReadyStage::Loading,
|
|
||||||
"DomReady" => UiReadyStage::DomReady,
|
|
||||||
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
|
||||||
"Ready" => UiReadyStage::Ready,
|
|
||||||
_ => {
|
|
||||||
log::warn!(target: "app", "未知的UI加载阶段: {stage}");
|
|
||||||
return Err(format!("未知的UI加载阶段: {stage}"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
crate::utils::resolve::update_ui_ready_stage(stage_enum);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 重置UI就绪状态
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn reset_ui_ready_state() -> CmdResult<()> {
|
|
||||||
log::info!(target: "app", "重置UI就绪状态");
|
|
||||||
crate::utils::resolve::reset_ui_ready();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::{config::*, core::*, feat, module::mihomo::MihomoManager, wrap_err};
|
||||||
config::*, core::*, feat, module::mihomo::MihomoManager, process::AsyncHandler, wrap_err,
|
|
||||||
};
|
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
|
|
||||||
/// 复制Clash环境变量
|
/// 复制Clash环境变量
|
||||||
@@ -26,8 +24,7 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
|||||||
/// 修改Clash模式
|
/// 修改Clash模式
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||||
feat::change_clash_mode(payload);
|
Ok(feat::change_clash_mode(payload))
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 切换Clash核心
|
/// 切换Clash核心
|
||||||
@@ -40,21 +37,10 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// 切换内核后重启内核
|
log::info!(target: "app", "core changed to {clash_core}");
|
||||||
match CoreManager::global().restart_core().await {
|
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||||
Ok(_) => {
|
handle::Handle::refresh_clash();
|
||||||
log::info!(target: "app", "core changed and restarted to {clash_core}");
|
Ok(None)
|
||||||
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let error_msg = format!("Core changed but failed to restart: {err}");
|
|
||||||
log::error!(target: "app", "{error_msg}");
|
|
||||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
|
||||||
Ok(Some(error_msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error_msg = err.to_string();
|
let error_msg = err.to_string();
|
||||||
@@ -65,18 +51,6 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 启动核心
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn start_core() -> CmdResult {
|
|
||||||
wrap_err!(CoreManager::global().start_core().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 关闭核心
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn stop_core() -> CmdResult {
|
|
||||||
wrap_err!(CoreManager::global().stop_core().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 重启核心
|
/// 重启核心
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn restart_core() -> CmdResult {
|
pub async fn restart_core() -> CmdResult {
|
||||||
@@ -116,7 +90,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
|||||||
// 保存DNS配置到文件
|
// 保存DNS配置到文件
|
||||||
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
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())?;
|
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
|
||||||
log::info!(target: "app", "DNS config saved to {dns_path:?}");
|
log::info!(target: "app", "DNS config saved to {:?}", dns_path);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -124,20 +98,19 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
|||||||
/// 应用或撤销DNS配置
|
/// 应用或撤销DNS配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||||
use crate::{
|
use crate::config::Config;
|
||||||
config::Config,
|
use crate::core::{handle, CoreManager};
|
||||||
core::{handle, CoreManager},
|
use crate::utils::dirs;
|
||||||
utils::dirs,
|
use tauri::async_runtime;
|
||||||
};
|
|
||||||
|
|
||||||
// 使用spawn来处理异步操作
|
// 使用spawn来处理异步操作
|
||||||
AsyncHandler::spawn(move || async move {
|
async_runtime::spawn(async move {
|
||||||
if apply {
|
if apply {
|
||||||
// 读取DNS配置文件
|
// 读取DNS配置文件
|
||||||
let dns_path = match dirs::app_home_dir() {
|
let dns_path = match dirs::app_home_dir() {
|
||||||
Ok(path) => path.join("dns_config.yaml"),
|
Ok(path) => path.join("dns_config.yaml"),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to get home dir: {e}");
|
log::error!(target: "app", "Failed to get home dir: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -150,7 +123,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to read DNS config: {e}");
|
log::error!(target: "app", "Failed to read DNS config: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -163,7 +136,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
patch
|
patch
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to parse DNS config: {e}");
|
log::error!(target: "app", "Failed to parse DNS config: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -178,13 +151,13 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
|
|
||||||
// 首先重新生成配置
|
// 首先重新生成配置
|
||||||
if let Err(err) = Config::generate().await {
|
if let Err(err) = Config::generate().await {
|
||||||
log::error!(target: "app", "Failed to regenerate config with DNS: {err}");
|
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 然后应用新配置
|
// 然后应用新配置
|
||||||
if let Err(err) = CoreManager::global().update_config().await {
|
if let Err(err) = CoreManager::global().update_config().await {
|
||||||
log::error!(target: "app", "Failed to apply config with DNS: {err}");
|
log::error!(target: "app", "Failed to apply config with DNS: {}", err);
|
||||||
} else {
|
} else {
|
||||||
log::info!(target: "app", "DNS config successfully applied");
|
log::info!(target: "app", "DNS config successfully applied");
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
@@ -196,7 +169,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
|
|
||||||
// 重新生成配置
|
// 重新生成配置
|
||||||
if let Err(err) = Config::generate().await {
|
if let Err(err) = Config::generate().await {
|
||||||
log::error!(target: "app", "Failed to regenerate config: {err}");
|
log::error!(target: "app", "Failed to regenerate config: {}", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +180,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "Failed to apply regenerated config: {err}");
|
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,25 +218,3 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
|
|||||||
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
|
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 验证DNS配置文件
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
|
||||||
use crate::{core::CoreManager, utils::dirs};
|
|
||||||
|
|
||||||
let app_dir = dirs::app_home_dir().map_err(|e| e.to_string())?;
|
|
||||||
let dns_path = app_dir.join("dns_config.yaml");
|
|
||||||
let dns_path_str = dns_path.to_str().unwrap_or_default();
|
|
||||||
|
|
||||||
if !dns_path.exists() {
|
|
||||||
return Ok((false, "DNS config file not found".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
match CoreManager::global()
|
|
||||||
.validate_config_file(dns_path_str, None)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(result) => Ok(result),
|
|
||||||
Err(e) => Err(e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
use crate::module::lightweight;
|
|
||||||
|
|
||||||
use super::CmdResult;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn entry_lightweight_mode() -> CmdResult {
|
|
||||||
lightweight::entry_lightweight_mode();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn exit_lightweight_mode() -> CmdResult {
|
|
||||||
lightweight::exit_lightweight_mode();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -4,35 +4,29 @@ use anyhow::Result;
|
|||||||
pub type CmdResult<T = ()> = Result<T, String>;
|
pub type CmdResult<T = ()> = Result<T, String>;
|
||||||
|
|
||||||
// Command modules
|
// Command modules
|
||||||
pub mod app;
|
|
||||||
pub mod clash;
|
|
||||||
pub mod lightweight;
|
|
||||||
pub mod media_unlock_checker;
|
|
||||||
pub mod network;
|
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod proxy;
|
pub mod validate;
|
||||||
|
pub mod uwp;
|
||||||
|
pub mod webdav;
|
||||||
|
pub mod app;
|
||||||
|
pub mod network;
|
||||||
|
pub mod clash;
|
||||||
|
pub mod verge;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
pub mod save_profile;
|
pub mod save_profile;
|
||||||
pub mod service;
|
|
||||||
pub mod system;
|
pub mod system;
|
||||||
pub mod uwp;
|
pub mod proxy;
|
||||||
pub mod validate;
|
|
||||||
pub mod verge;
|
|
||||||
pub mod webdav;
|
|
||||||
|
|
||||||
// Re-export all command functions for backwards compatibility
|
// 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 profile::*;
|
||||||
pub use proxy::*;
|
pub use validate::*;
|
||||||
|
pub use uwp::*;
|
||||||
|
pub use webdav::*;
|
||||||
|
pub use app::*;
|
||||||
|
pub use network::*;
|
||||||
|
pub use clash::*;
|
||||||
|
pub use verge::*;
|
||||||
pub use runtime::*;
|
pub use runtime::*;
|
||||||
pub use save_profile::*;
|
pub use save_profile::*;
|
||||||
pub use service::*;
|
|
||||||
pub use system::*;
|
pub use system::*;
|
||||||
pub use uwp::*;
|
pub use proxy::*;
|
||||||
pub use validate::*;
|
|
||||||
pub use verge::*;
|
|
||||||
pub use webdav::*;
|
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
use super::CmdResult;
|
|
||||||
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
|
|
||||||
use crate::wrap_err;
|
use crate::wrap_err;
|
||||||
use network_interface::NetworkInterface;
|
use super::CmdResult;
|
||||||
|
use sysproxy::{Autoproxy, Sysproxy};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
|
use network_interface::NetworkInterface;
|
||||||
|
|
||||||
/// get the system proxy
|
/// get the system proxy
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||||
log::debug!(target: "app", "异步获取系统代理配置");
|
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
||||||
|
|
||||||
let current = AsyncProxyQuery::get_system_proxy().await;
|
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
let mut map = Mapping::new();
|
||||||
map.insert("enable".into(), current.enable.into());
|
map.insert("enable".into(), current.enable.into());
|
||||||
map.insert(
|
map.insert(
|
||||||
@@ -19,50 +16,21 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
|||||||
);
|
);
|
||||||
map.insert("bypass".into(), current.bypass.into());
|
map.insert("bypass".into(), current.bypass.into());
|
||||||
|
|
||||||
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取自动代理配置
|
/// get the system proxy
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
pub fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||||
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
|
let current = wrap_err!(Autoproxy::get_auto_proxy())?;
|
||||||
|
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
|
|
||||||
let current = proxy_manager.get_auto_proxy_cached();
|
|
||||||
// 异步请求更新,立即返回缓存数据
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = proxy_manager.get_auto_proxy_async().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
let mut map = Mapping::new();
|
||||||
map.insert("enable".into(), current.enable.into());
|
map.insert("enable".into(), current.enable.into());
|
||||||
map.insert("url".into(), current.url.clone().into());
|
map.insert("url".into(), current.url.into());
|
||||||
|
|
||||||
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取系统主机名
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_system_hostname() -> CmdResult<String> {
|
|
||||||
use gethostname::gethostname;
|
|
||||||
|
|
||||||
// 获取系统主机名,处理可能的非UTF-8字符
|
|
||||||
let hostname = match gethostname().into_string() {
|
|
||||||
Ok(name) => name,
|
|
||||||
Err(os_string) => {
|
|
||||||
// 对于包含非UTF-8的主机名,使用调试格式化
|
|
||||||
let fallback = format!("{os_string:?}");
|
|
||||||
// 去掉可能存在的引号
|
|
||||||
fallback.trim_matches('"').to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(hostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取网络接口列表
|
/// 获取网络接口列表
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_network_interfaces() -> Vec<String> {
|
pub fn get_network_interfaces() -> Vec<String> {
|
||||||
@@ -78,7 +46,8 @@ pub fn get_network_interfaces() -> Vec<String> {
|
|||||||
/// 获取网络接口详细信息
|
/// 获取网络接口详细信息
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||||
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
|
use network_interface::NetworkInterface;
|
||||||
|
use network_interface::NetworkInterfaceConfig;
|
||||||
|
|
||||||
let names = get_network_interfaces();
|
let names = get_network_interfaces();
|
||||||
let interfaces = wrap_err!(NetworkInterface::show())?;
|
let interfaces = wrap_err!(NetworkInterface::show())?;
|
||||||
|
|||||||
@@ -1,152 +1,47 @@
|
|||||||
use super::CmdResult;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IProfiles, PrfItem, PrfOption},
|
config::*,
|
||||||
core::{handle, timer::Timer, tray::Tray, CoreManager},
|
core::*,
|
||||||
feat, logging, ret_err,
|
feat,
|
||||||
utils::{dirs, help, logging::Type},
|
utils::{dirs, help},
|
||||||
wrap_err,
|
log_err, ret_err, wrap_err,
|
||||||
};
|
};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use super::CmdResult;
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::sync::{Mutex, RwLock};
|
|
||||||
|
|
||||||
// 全局互斥锁防止并发配置更新
|
/// 获取配置文件列表
|
||||||
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
|
||||||
|
|
||||||
// 全局请求序列号跟踪,用于避免队列化执行
|
|
||||||
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
|
||||||
|
|
||||||
static CURRENT_PROCESSING_PROFILE: RwLock<Option<String>> = RwLock::const_new(None);
|
|
||||||
|
|
||||||
/// 清理配置处理状态
|
|
||||||
async fn cleanup_processing_state(sequence: u64, reason: &str) {
|
|
||||||
*CURRENT_PROCESSING_PROFILE.write().await = None;
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"{},清理状态,序列号: {}",
|
|
||||||
reason,
|
|
||||||
sequence
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取配置文件避免锁竞争
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
// 策略1: 尝试快速获取latest数据
|
let _ = tray::Tray::global().update_menu();
|
||||||
let latest_result = tokio::time::timeout(
|
Ok(Config::profiles().data().clone())
|
||||||
Duration::from_millis(500),
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
let profiles = Config::profiles();
|
|
||||||
let latest = profiles.latest();
|
|
||||||
IProfiles {
|
|
||||||
current: latest.current.clone(),
|
|
||||||
items: latest.items.clone(),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match latest_result {
|
|
||||||
Ok(Ok(profiles)) => {
|
|
||||||
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
|
|
||||||
return Ok(profiles);
|
|
||||||
}
|
|
||||||
Ok(Err(join_err)) => {
|
|
||||||
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 策略2: 如果快速获取失败,尝试获取data()
|
|
||||||
let data_result = tokio::time::timeout(
|
|
||||||
Duration::from_secs(2),
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
let profiles = Config::profiles();
|
|
||||||
let data = profiles.data();
|
|
||||||
IProfiles {
|
|
||||||
current: data.current.clone(),
|
|
||||||
items: data.items.clone(),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match data_result {
|
|
||||||
Ok(Ok(profiles)) => {
|
|
||||||
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
|
|
||||||
return Ok(profiles);
|
|
||||||
}
|
|
||||||
Ok(Err(join_err)) => {
|
|
||||||
logging!(
|
|
||||||
error,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"获取draft配置任务失败: {}",
|
|
||||||
join_err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 策略3: fallback,尝试重新创建配置
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"所有获取配置策略都失败,尝试fallback"
|
|
||||||
);
|
|
||||||
|
|
||||||
match tokio::task::spawn_blocking(IProfiles::new).await {
|
|
||||||
Ok(profiles) => {
|
|
||||||
logging!(info, Type::Cmd, true, "使用fallback配置成功");
|
|
||||||
Ok(profiles)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
|
|
||||||
// 返回空配置避免崩溃
|
|
||||||
Ok(IProfiles {
|
|
||||||
current: None,
|
|
||||||
items: Some(vec![]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 增强配置文件
|
/// 增强配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn enhance_profiles() -> CmdResult {
|
pub async fn enhance_profiles() -> CmdResult {
|
||||||
wrap_err!(feat::enhance_profiles().await)?;
|
match CoreManager::global().update_config().await {
|
||||||
handle::Handle::refresh_clash();
|
Ok((true, _)) => {
|
||||||
Ok(())
|
println!("[enhance_profiles] 配置更新成功");
|
||||||
|
log_err!(tray::Tray::global().update_tooltip());
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok((false, error_msg)) => {
|
||||||
|
println!("[enhance_profiles] 配置验证失败: {}", error_msg);
|
||||||
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[enhance_profiles] 更新过程发生错误: {}", e);
|
||||||
|
handle::Handle::notice_message("config_validate::process_terminated", &e.to_string());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导入配置文件
|
/// 导入配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||||
let existing_uid = {
|
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||||
let profiles = Config::profiles();
|
wrap_err!(Config::profiles().data().append_item(item))
|
||||||
let profiles = profiles.latest();
|
|
||||||
|
|
||||||
profiles.items.as_ref()
|
|
||||||
.and_then(|items| items.iter().find(|item| item.url.as_deref() == Some(&url)))
|
|
||||||
.and_then(|item| item.uid.clone())
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(uid) = existing_uid {
|
|
||||||
logging!(info, Type::Cmd, true, "The profile with URL {} already exists (UID: {}). Running the update...", url, uid);
|
|
||||||
update_profile(uid, option).await
|
|
||||||
} else {
|
|
||||||
logging!(info, Type::Cmd, true, "Profile with URL {} not found. Create a new one...", url);
|
|
||||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
|
||||||
wrap_err!(Config::profiles().data().append_item(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重新排序配置文件
|
/// 重新排序配置文件
|
||||||
@@ -165,17 +60,13 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
|||||||
/// 更新配置文件
|
/// 更新配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||||
wrap_err!(feat::update_profile(index, option, Some(true)).await)
|
wrap_err!(feat::update_profile(index, option).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除配置文件
|
/// 删除配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_profile(index: String) -> CmdResult {
|
pub async fn delete_profile(index: String) -> CmdResult {
|
||||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||||
|
|
||||||
// 删除后自动清理冗余文件
|
|
||||||
let _ = Config::profiles().latest().auto_cleanup();
|
|
||||||
|
|
||||||
if should_update {
|
if should_update {
|
||||||
wrap_err!(CoreManager::global().update_config().await)?;
|
wrap_err!(CoreManager::global().update_config().await)?;
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
@@ -185,322 +76,36 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
|||||||
|
|
||||||
/// 修改profiles的配置
|
/// 修改profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
pub async fn patch_profiles_config(
|
||||||
// 为当前请求分配序列号
|
profiles: IProfiles
|
||||||
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
) -> CmdResult<bool> {
|
||||||
let target_profile = profiles.current.clone();
|
println!("[cmd配置patch] 开始修改配置文件");
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
|
|
||||||
current_sequence,
|
|
||||||
target_profile
|
|
||||||
);
|
|
||||||
|
|
||||||
let mutex_result =
|
|
||||||
tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await;
|
|
||||||
|
|
||||||
let _guard = match mutex_result {
|
|
||||||
Ok(guard) => guard,
|
|
||||||
Err(_) => {
|
|
||||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
|
||||||
if current_sequence < latest_sequence {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"检测到更新的请求 (序列号: {} < {}),放弃当前请求",
|
|
||||||
current_sequence,
|
|
||||||
latest_sequence
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"强制获取锁以处理最新请求: {}",
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
PROFILE_UPDATE_MUTEX.lock().await
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
|
||||||
if current_sequence < latest_sequence {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
|
||||||
current_sequence,
|
|
||||||
latest_sequence
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存当前配置,以便在验证失败时恢复
|
// 保存当前配置,以便在验证失败时恢复
|
||||||
let current_profile = Config::profiles().latest().current.clone();
|
let current_profile = Config::profiles().latest().current.clone();
|
||||||
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
|
println!("[cmd配置patch] 当前配置: {:?}", 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 config_file_result = {
|
|
||||||
let profiles_config = Config::profiles();
|
|
||||||
let profiles_data = profiles_config.latest();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 超时保护
|
|
||||||
let file_read_result = tokio::time::timeout(
|
|
||||||
Duration::from_secs(5),
|
|
||||||
tokio::fs::read_to_string(&file_path),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match file_read_result {
|
|
||||||
Ok(Ok(content)) => {
|
|
||||||
let yaml_parse_result = tokio::task::spawn_blocking(move || {
|
|
||||||
serde_yaml::from_str::<serde_yaml::Value>(&content)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match yaml_parse_result {
|
|
||||||
Ok(Ok(_)) => {
|
|
||||||
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
|
||||||
}
|
|
||||||
Ok(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(join_err) => {
|
|
||||||
let error_msg = format!("YAML解析任务失败: {join_err}");
|
|
||||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
|
||||||
handle::Handle::notice_message(
|
|
||||||
"config_validate::yaml_parse_error",
|
|
||||||
&error_msg,
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(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);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
|
||||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
|
||||||
handle::Handle::notice_message(
|
|
||||||
"config_validate::file_read_timeout",
|
|
||||||
&error_msg,
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查请求有效性
|
|
||||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
|
||||||
if current_sequence < latest_sequence {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
|
||||||
current_sequence,
|
|
||||||
latest_sequence
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref profile) = target_profile {
|
|
||||||
*CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone());
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"设置当前处理profile: {}, 序列号: {}",
|
|
||||||
profile,
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新profiles配置
|
// 更新profiles配置
|
||||||
logging!(
|
println!("[cmd配置patch] 正在更新配置草稿");
|
||||||
info,
|
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"正在更新配置草稿,序列号: {}",
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
|
|
||||||
let current_value = profiles.current.clone();
|
|
||||||
|
|
||||||
let _ = Config::profiles().draft().patch_config(profiles);
|
|
||||||
|
|
||||||
// 在调用内核前再次验证请求有效性
|
|
||||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
|
||||||
if current_sequence < latest_sequence {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
|
||||||
current_sequence,
|
|
||||||
latest_sequence
|
|
||||||
);
|
|
||||||
Config::profiles().discard();
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为配置更新添加超时保护
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"开始内核配置更新,序列号: {}",
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
let update_result = tokio::time::timeout(
|
|
||||||
Duration::from_secs(30), // 30秒超时
|
|
||||||
CoreManager::global().update_config(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// 更新配置并进行验证
|
// 更新配置并进行验证
|
||||||
match update_result {
|
match CoreManager::global().update_config().await {
|
||||||
Ok(Ok((true, _))) => {
|
Ok((true, _)) => {
|
||||||
// 内核操作完成后再次检查请求有效性
|
println!("[cmd配置patch] 配置更新成功");
|
||||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
|
||||||
if current_sequence < latest_sequence {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
|
|
||||||
current_sequence,
|
|
||||||
latest_sequence
|
|
||||||
);
|
|
||||||
Config::profiles().discard();
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"配置更新成功,序列号: {}",
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
Config::profiles().apply();
|
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
|
let _ = tray::Tray::global().update_tooltip();
|
||||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
Config::profiles().apply();
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
if let Err(e) = super::proxy::force_refresh_proxies().await {
|
|
||||||
log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
|
||||||
if let Err(e) = Tray::global().update_tooltip() {
|
|
||||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = Tray::global().update_menu() {
|
|
||||||
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存配置文件
|
|
||||||
if let Err(e) = Config::profiles().data().save_file() {
|
|
||||||
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即通知前端配置变更
|
|
||||||
if let Some(current) = ¤t_value {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"向前端发送配置变更事件: {}, 序列号: {}",
|
|
||||||
current,
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
handle::Handle::notify_profile_changed(current.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_processing_state(current_sequence, "配置切换完成").await;
|
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok(Ok((false, error_msg))) => {
|
Ok((false, error_msg)) => {
|
||||||
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
|
println!("[cmd配置patch] 配置验证失败: {}", error_msg);
|
||||||
Config::profiles().discard();
|
Config::profiles().discard();
|
||||||
|
|
||||||
// 如果验证失败,恢复到之前的配置
|
// 如果验证失败,恢复到之前的配置
|
||||||
if let Some(prev_profile) = current_profile {
|
if let Some(prev_profile) = current_profile {
|
||||||
logging!(
|
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile);
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"尝试恢复到之前的配置: {}",
|
|
||||||
prev_profile
|
|
||||||
);
|
|
||||||
let restore_profiles = IProfiles {
|
let restore_profiles = IProfiles {
|
||||||
current: Some(prev_profile),
|
current: Some(prev_profile),
|
||||||
items: None,
|
items: None,
|
||||||
@@ -508,73 +113,18 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
// 静默恢复,不触发验证
|
// 静默恢复,不触发验证
|
||||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
println!("[cmd配置patch] 成功恢复到之前的配置");
|
||||||
if let Err(e) = Config::profiles().data().save_file() {
|
|
||||||
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送验证错误通知
|
// 发送验证错误通知
|
||||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
|
|
||||||
cleanup_processing_state(current_sequence, "配置验证失败").await;
|
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Err(e) => {
|
||||||
logging!(
|
println!("[cmd配置patch] 更新过程发生错误: {}", e);
|
||||||
warn,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"更新过程发生错误: {}, 序列号: {}",
|
|
||||||
e,
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
Config::profiles().discard();
|
Config::profiles().discard();
|
||||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
handle::Handle::notice_message("config_validate::boot_error", &e.to_string());
|
||||||
|
|
||||||
cleanup_processing_state(current_sequence, "更新过程错误").await;
|
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// 超时处理
|
|
||||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
|
||||||
logging!(
|
|
||||||
error,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"{}, 序列号: {}",
|
|
||||||
timeout_msg,
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
Config::profiles().discard();
|
|
||||||
|
|
||||||
if let Some(prev_profile) = current_profile {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
|
|
||||||
prev_profile,
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
let restore_profiles = IProfiles {
|
|
||||||
current: Some(prev_profile),
|
|
||||||
items: None,
|
|
||||||
};
|
|
||||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
|
||||||
Config::profiles().apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
|
||||||
|
|
||||||
cleanup_processing_state(current_sequence, "配置更新超时").await;
|
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,47 +134,16 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config_by_profile_index(
|
pub async fn patch_profiles_config_by_profile_index(
|
||||||
_app_handle: tauri::AppHandle,
|
_app_handle: tauri::AppHandle,
|
||||||
profile_index: String,
|
profile_index: String
|
||||||
) -> CmdResult<bool> {
|
) -> CmdResult<bool> {
|
||||||
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
|
let profiles = IProfiles{current: Some(profile_index), items: None};
|
||||||
|
|
||||||
let profiles = IProfiles {
|
|
||||||
current: Some(profile_index),
|
|
||||||
items: None,
|
|
||||||
};
|
|
||||||
patch_profiles_config(profiles).await
|
patch_profiles_config(profiles).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改某个profile item的
|
/// 修改某个profile item的
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||||
// 保存修改前检查是否有更新 update_interval
|
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||||
let update_interval_changed =
|
|
||||||
if let Ok(old_profile) = Config::profiles().latest().get_item(&index) {
|
|
||||||
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
|
|
||||||
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
|
|
||||||
old_interval != new_interval
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存修改
|
|
||||||
wrap_err!(Config::profiles().data().patch_item(index.clone(), profile))?;
|
|
||||||
|
|
||||||
// 如果更新间隔变更,异步刷新定时器
|
|
||||||
if update_interval_changed {
|
|
||||||
let index_clone = index.clone();
|
|
||||||
crate::process::AsyncHandler::spawn(move || async move {
|
|
||||||
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
|
|
||||||
if let Err(e) = crate::core::Timer::global().refresh() {
|
|
||||||
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
|
|
||||||
} else {
|
|
||||||
// 刷新成功后发送自定义事件,不触发配置重载
|
|
||||||
crate::core::handle::Handle::notify_timer_updated(index_clone);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,56 +174,3 @@ pub fn read_profile_file(index: String) -> CmdResult<String> {
|
|||||||
let data = wrap_err!(item.read_file())?;
|
let data = wrap_err!(item.read_file())?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取下一次更新时间
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
|
||||||
let timer = Timer::global();
|
|
||||||
let next_time = timer.get_next_update_time(&uid);
|
|
||||||
Ok(next_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn update_profiles_on_startup() -> CmdResult {
|
|
||||||
logging!(info, Type::Cmd, true, "Checking profiles for updates at startup...");
|
|
||||||
|
|
||||||
let profiles_to_update = {
|
|
||||||
let profiles = Config::profiles();
|
|
||||||
let profiles = profiles.latest();
|
|
||||||
|
|
||||||
profiles.items.as_ref()
|
|
||||||
.map_or_else(
|
|
||||||
Vec::new,
|
|
||||||
|items| items.iter()
|
|
||||||
.filter(|item| item.option.as_ref().is_some_and(|opt| opt.update_always == Some(true)))
|
|
||||||
.filter_map(|item| item.uid.clone())
|
|
||||||
.collect()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if profiles_to_update.is_empty() {
|
|
||||||
logging!(info, Type::Cmd, true, "No profiles to update immediately.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(info, Type::Cmd, true, "Found profiles to update: {:?}", profiles_to_update);
|
|
||||||
|
|
||||||
let mut update_futures = Vec::new();
|
|
||||||
for uid in profiles_to_update {
|
|
||||||
update_futures.push(update_profile(uid, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
let results = futures::future::join_all(update_futures).await;
|
|
||||||
|
|
||||||
|
|
||||||
if results.iter().any(|res| res.is_ok()) {
|
|
||||||
logging!(info, Type::Cmd, true, "The startup update is complete, restart the kernel...");
|
|
||||||
CoreManager::global().update_config().await.map_err(|e| e.to_string())?;
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
} else {
|
|
||||||
logging!(warn, Type::Cmd, true, "All updates completed with errors on startup.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,99 +1,16 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState};
|
use crate::module::mihomo::MihomoManager;
|
||||||
use std::{
|
|
||||||
sync::Mutex,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3);
|
|
||||||
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let manager = MihomoManager::global();
|
let mannager = MihomoManager::global();
|
||||||
|
mannager.refresh_proxies().await.unwrap();
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
Ok(mannager.get_proxies())
|
||||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
|
||||||
|
|
||||||
let should_refresh = {
|
|
||||||
let mut state = cmd_proxy_state.lock().unwrap();
|
|
||||||
let now = Instant::now();
|
|
||||||
if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL {
|
|
||||||
state.need_refresh = true;
|
|
||||||
state.last_refresh_time = now;
|
|
||||||
}
|
|
||||||
state.need_refresh
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_refresh {
|
|
||||||
let proxies = manager.get_refresh_proxies().await?;
|
|
||||||
{
|
|
||||||
let mut state = cmd_proxy_state.lock().unwrap();
|
|
||||||
state.proxies = Box::new(proxies);
|
|
||||||
state.need_refresh = false;
|
|
||||||
}
|
|
||||||
log::debug!(target: "app", "proxies刷新成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
let proxies = {
|
|
||||||
let state = cmd_proxy_state.lock().unwrap();
|
|
||||||
state.proxies.clone()
|
|
||||||
};
|
|
||||||
Ok(*proxies)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 强制刷新代理缓存用于profile切换
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
|
||||||
let manager = MihomoManager::global();
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
|
||||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
|
||||||
|
|
||||||
log::debug!(target: "app", "强制刷新代理缓存");
|
|
||||||
|
|
||||||
let proxies = manager.get_refresh_proxies().await?;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut state = cmd_proxy_state.lock().unwrap();
|
|
||||||
state.proxies = Box::new(proxies.clone());
|
|
||||||
state.need_refresh = false;
|
|
||||||
state.last_refresh_time = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!(target: "app", "强制刷新代理缓存完成");
|
|
||||||
Ok(proxies)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let mannager = MihomoManager::global();
|
||||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
mannager.refresh_providers_proxies().await.unwrap();
|
||||||
|
Ok(mannager.get_providers_proxies())
|
||||||
let should_refresh = {
|
|
||||||
let mut state = cmd_proxy_state.lock().unwrap();
|
|
||||||
let now = Instant::now();
|
|
||||||
if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL {
|
|
||||||
state.need_refresh = true;
|
|
||||||
state.last_refresh_time = now;
|
|
||||||
}
|
|
||||||
state.need_refresh
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_refresh {
|
|
||||||
let manager = MihomoManager::global();
|
|
||||||
let providers = manager.get_providers_proxies().await?;
|
|
||||||
{
|
|
||||||
let mut state = cmd_proxy_state.lock().unwrap();
|
|
||||||
state.providers_proxies = Box::new(providers);
|
|
||||||
state.need_refresh = false;
|
|
||||||
}
|
|
||||||
log::debug!(target: "app", "providers_proxies刷新成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
let providers_proxies = {
|
|
||||||
let state = cmd_proxy_state.lock().unwrap();
|
|
||||||
state.providers_proxies.clone()
|
|
||||||
};
|
|
||||||
Ok(*providers_proxies)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
use crate::{
|
||||||
|
config::*,
|
||||||
|
wrap_err,
|
||||||
|
};
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{config::*, wrap_err};
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
|
||||||
/// 获取运行时配置
|
/// 获取运行时配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use super::CmdResult;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::*,
|
config::*,
|
||||||
core::*,
|
core::*,
|
||||||
logging,
|
utils::dirs,
|
||||||
utils::{dirs, logging::Type},
|
|
||||||
wrap_err,
|
wrap_err,
|
||||||
};
|
};
|
||||||
|
use super::CmdResult;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
/// 保存profiles的配置
|
/// 保存profiles的配置
|
||||||
@@ -21,7 +20,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
let profiles_guard = profiles.latest();
|
let profiles_guard = profiles.latest();
|
||||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||||
// 确定是否为merge类型文件
|
// 确定是否为merge类型文件
|
||||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
let is_merge = item.itype.as_ref().map_or(false, |t| t == "merge");
|
||||||
let content = wrap_err!(item.read_file())?;
|
let content = wrap_err!(item.read_file())?;
|
||||||
let path = item.file.clone().ok_or("file field is null")?;
|
let path = item.file.clone().ok_or("file field is null")?;
|
||||||
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||||
@@ -30,56 +29,25 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
|
|
||||||
// 保存新的配置文件
|
// 保存新的配置文件
|
||||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||||
|
|
||||||
let file_path_str = file_path.to_string_lossy().to_string();
|
let file_path_str = file_path.to_string_lossy().to_string();
|
||||||
logging!(
|
println!("[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}", file_path_str, is_merge_file);
|
||||||
info,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
|
|
||||||
file_path_str,
|
|
||||||
is_merge_file
|
|
||||||
);
|
|
||||||
|
|
||||||
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||||
if is_merge_file {
|
if is_merge_file {
|
||||||
logging!(
|
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
|
||||||
info,
|
match CoreManager::global().validate_config_file(&file_path_str, Some(true)).await {
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[cmd配置save] 检测到merge文件,只进行语法验证"
|
|
||||||
);
|
|
||||||
match CoreManager::global()
|
|
||||||
.validate_config_file(&file_path_str, Some(true))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((true, _)) => {
|
Ok((true, _)) => {
|
||||||
logging!(
|
println!("[cmd配置save] merge文件语法验证通过");
|
||||||
info,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[cmd配置save] merge文件语法验证通过"
|
|
||||||
);
|
|
||||||
// 成功后尝试更新整体配置
|
// 成功后尝试更新整体配置
|
||||||
if let Err(e) = CoreManager::global().update_config().await {
|
if let Err(e) = CoreManager::global().update_config().await {
|
||||||
logging!(
|
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
|
||||||
warn,
|
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[cmd配置save] 更新整体配置时发生错误: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
Ok((false, error_msg)) => {
|
||||||
logging!(
|
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
|
||||||
warn,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[cmd配置save] merge文件语法验证失败: {}",
|
|
||||||
error_msg
|
|
||||||
);
|
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
// 发送合并文件专用错误通知
|
// 发送合并文件专用错误通知
|
||||||
@@ -88,75 +56,53 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(
|
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||||
error,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[cmd配置save] 验证过程发生错误: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
return Err(e.to_string());
|
return Err(e.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非merge文件使用完整验证流程
|
// 非merge文件使用完整验证流程
|
||||||
match CoreManager::global()
|
match CoreManager::global().validate_config_file(&file_path_str, None).await {
|
||||||
.validate_config_file(&file_path_str, None)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((true, _)) => {
|
Ok((true, _)) => {
|
||||||
logging!(info, Type::Config, true, "[cmd配置save] 验证成功");
|
println!("[cmd配置save] 验证成功");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
Ok((false, error_msg)) => {
|
||||||
logging!(
|
println!("[cmd配置save] 验证失败: {}", error_msg);
|
||||||
warn,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[cmd配置save] 验证失败: {}",
|
|
||||||
error_msg
|
|
||||||
);
|
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
|
||||||
// 智能判断错误类型
|
// 智能判断错误类型
|
||||||
let is_script_error = file_path_str.ends_with(".js")
|
let is_script_error = file_path_str.ends_with(".js") ||
|
||||||
|| error_msg.contains("Script syntax error")
|
error_msg.contains("Script syntax error") ||
|
||||||
|| error_msg.contains("Script must contain a main function")
|
error_msg.contains("Script must contain a main function") ||
|
||||||
|| error_msg.contains("Failed to read script file");
|
error_msg.contains("Failed to read script file");
|
||||||
|
|
||||||
if error_msg.contains("YAML syntax error")
|
if error_msg.contains("YAML syntax error") ||
|
||||||
|| error_msg.contains("Failed to read file:")
|
error_msg.contains("Failed to read file:") ||
|
||||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
(!file_path_str.ends_with(".js") && !is_script_error) {
|
||||||
{
|
|
||||||
// 普通YAML错误使用YAML通知处理
|
// 普通YAML错误使用YAML通知处理
|
||||||
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
|
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||||
let result = (false, error_msg.clone());
|
let result = (false, error_msg.clone());
|
||||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||||
} else if is_script_error {
|
} else if is_script_error {
|
||||||
// 脚本错误使用专门的通知处理
|
// 脚本错误使用专门的通知处理
|
||||||
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
|
println!("[cmd配置save] 脚本文件验证失败,发送通知");
|
||||||
let result = (false, error_msg.clone());
|
let result = (false, error_msg.clone());
|
||||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||||
} else {
|
} else {
|
||||||
// 普通配置错误使用一般通知
|
// 普通配置错误使用一般通知
|
||||||
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
|
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(
|
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||||
error,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[cmd配置save] 验证过程发生错误: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
Err(e.to_string())
|
Err(e.to_string())
|
||||||
|
|||||||
@@ -1,48 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn is_service_available() -> CmdResult<bool> {
|
|
||||||
service::is_service_available()
|
|
||||||
.await
|
|
||||||
.map(|_| true)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
@@ -1,94 +1,35 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::core::handle;
|
||||||
core::{handle, CoreManager},
|
use crate::module::sysinfo::PlatformSpecification;
|
||||||
module::sysinfo::PlatformSpecification,
|
|
||||||
};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use std::{
|
|
||||||
sync::atomic::{AtomicI64, Ordering},
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
|
use crate::{core::{self, CoreManager, service}, wrap_err};
|
||||||
// 存储应用启动时间的全局变量
|
|
||||||
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]
|
#[tauri::command]
|
||||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||||
let sysinfo = PlatformSpecification::new_async().await;
|
let sysinfo = PlatformSpecification::new();
|
||||||
let info = format!("{sysinfo:?}");
|
let info = format!("{:?}", sysinfo);
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let cliboard = app_handle.clipboard();
|
let cliboard = app_handle.clipboard();
|
||||||
if cliboard.write_text(info).is_err() {
|
|
||||||
|
if let Err(_) = cliboard.write_text(info) {
|
||||||
log::error!(target: "app", "Failed to write to clipboard");
|
log::error!(target: "app", "Failed to write to clipboard");
|
||||||
}
|
}
|
||||||
Ok(())
|
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]
|
#[tauri::command]
|
||||||
pub async fn get_running_mode() -> Result<String, String> {
|
pub async fn get_running_mode() -> Result<String, String> {
|
||||||
Ok(CoreManager::global().get_running_mode().await.to_string())
|
match CoreManager::global().get_running_mode().await {
|
||||||
}
|
core::RunningMode::Service => Ok("service".to_string()),
|
||||||
|
core::RunningMode::Sidecar => Ok("sidecar".to_string()),
|
||||||
/// 获取应用的运行时间(毫秒)
|
core::RunningMode::NotRunning => Ok("not_running".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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 安装/重装系统服务
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn install_service() -> CmdResult {
|
||||||
|
wrap_err!(service::reinstall_service().await)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use super::CmdResult;
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
mod platform {
|
mod platform {
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{core::win_uwp, wrap_err};
|
use crate::core::win_uwp;
|
||||||
|
use crate::wrap_err;
|
||||||
|
|
||||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
use crate::core::*;
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{core::*, logging, utils::logging::Type};
|
|
||||||
|
|
||||||
/// 发送脚本验证通知消息
|
/// 发送脚本验证通知消息
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -13,7 +13,7 @@ pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
|
|||||||
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
|
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
|
||||||
if !result.0 {
|
if !result.0 {
|
||||||
let error_msg = &result.1;
|
let error_msg = &result.1;
|
||||||
|
|
||||||
// 根据错误消息内容判断错误类型
|
// 根据错误消息内容判断错误类型
|
||||||
let status = if error_msg.starts_with("File not found:") {
|
let status = if error_msg.starts_with("File not found:") {
|
||||||
"config_validate::file_not_found"
|
"config_validate::file_not_found"
|
||||||
@@ -27,15 +27,8 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
|||||||
// 如果是其他类型错误,作为一般脚本错误处理
|
// 如果是其他类型错误,作为一般脚本错误处理
|
||||||
"config_validate::script_error"
|
"config_validate::script_error"
|
||||||
};
|
};
|
||||||
|
|
||||||
logging!(
|
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||||
warn,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"{} 验证失败: {}",
|
|
||||||
file_type,
|
|
||||||
error_msg
|
|
||||||
);
|
|
||||||
handle::Handle::notice_message(status, error_msg);
|
handle::Handle::notice_message(status, error_msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,25 +36,16 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
|||||||
/// 验证指定脚本文件
|
/// 验证指定脚本文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||||
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
|
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
||||||
|
|
||||||
match CoreManager::global()
|
match CoreManager::global().validate_config_file(&file_path, None).await {
|
||||||
.validate_config_file(&file_path, None)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
handle_script_validation_notice(&result, "脚本文件");
|
handle_script_validation_notice(&result, "脚本文件");
|
||||||
Ok(result.0) // 返回验证结果布尔值
|
Ok(result.0) // 返回验证结果布尔值
|
||||||
}
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_msg = e.to_string();
|
let error_msg = e.to_string();
|
||||||
logging!(
|
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
||||||
error,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"验证脚本文件过程发生错误: {}",
|
|
||||||
error_msg
|
|
||||||
);
|
|
||||||
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
@@ -73,18 +57,11 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
|||||||
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||||
if !result.0 {
|
if !result.0 {
|
||||||
let error_msg = &result.1;
|
let error_msg = &result.1;
|
||||||
logging!(
|
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
|
||||||
info,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[通知] 处理{}验证错误: {}",
|
|
||||||
file_type,
|
|
||||||
error_msg
|
|
||||||
);
|
|
||||||
|
|
||||||
// 检查是否为merge文件
|
// 检查是否为merge文件
|
||||||
let is_merge_file = file_type.contains("合并");
|
let is_merge_file = file_type.contains("合并");
|
||||||
|
|
||||||
// 根据错误消息内容判断错误类型
|
// 根据错误消息内容判断错误类型
|
||||||
let status = if error_msg.starts_with("File not found:") {
|
let status = if error_msg.starts_with("File not found:") {
|
||||||
"config_validate::file_not_found"
|
"config_validate::file_not_found"
|
||||||
@@ -116,23 +93,9 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
|||||||
"config_validate::yaml_error"
|
"config_validate::yaml_error"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
logging!(
|
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||||
warn,
|
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"{} 验证失败: {}",
|
|
||||||
file_type,
|
|
||||||
error_msg
|
|
||||||
);
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[通知] 发送通知: status={}, msg={}",
|
|
||||||
status,
|
|
||||||
error_msg
|
|
||||||
);
|
|
||||||
handle::Handle::notice_message(status, error_msg);
|
handle::Handle::notice_message(status, error_msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
use crate::{
|
||||||
|
config::*,
|
||||||
|
feat,
|
||||||
|
wrap_err,
|
||||||
|
};
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{config::*, feat, wrap_err};
|
|
||||||
|
|
||||||
/// 获取Verge配置
|
/// 获取Verge配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge_data = verge.data().clone();
|
let verge_data = verge.data().clone();
|
||||||
Ok(IVergeResponse::from(*verge_data))
|
Ok(IVergeResponse::from(verge_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改Verge配置
|
/// 修改Verge配置
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
use crate::{
|
||||||
|
core,
|
||||||
|
config::*,
|
||||||
|
feat,
|
||||||
|
wrap_err,
|
||||||
|
};
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{config::*, core, feat, wrap_err};
|
|
||||||
use reqwest_dav::list_cmd::ListFile;
|
use reqwest_dav::list_cmd::ListFile;
|
||||||
|
|
||||||
/// 保存 WebDAV 配置
|
/// 保存 WebDAV 配置
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ impl IClashTemp {
|
|||||||
map.insert(key.clone(), template.0.get(key).unwrap().clone());
|
map.insert(key.clone(), template.0.get(key).unwrap().clone());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 确保 secret 字段存在且不为空
|
|
||||||
if let Some(Value::String(s)) = map.get_mut("secret") {
|
|
||||||
if s.is_empty() {
|
|
||||||
*s = "set-your-secret".to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self(Self::guard(map))
|
Self(Self::guard(map))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -38,13 +32,13 @@ impl IClashTemp {
|
|||||||
pub fn template() -> Self {
|
pub fn template() -> Self {
|
||||||
let mut map = Mapping::new();
|
let mut map = Mapping::new();
|
||||||
let mut tun = Mapping::new();
|
let mut tun = Mapping::new();
|
||||||
let mut cors_map = Mapping::new();
|
|
||||||
tun.insert("enable".into(), false.into());
|
tun.insert("enable".into(), false.into());
|
||||||
tun.insert("stack".into(), "gvisor".into());
|
tun.insert("stack".into(), "mixed".into());
|
||||||
tun.insert("auto-route".into(), true.into());
|
tun.insert("auto-route".into(), true.into());
|
||||||
tun.insert("strict-route".into(), false.into());
|
tun.insert("strict-route".into(), false.into());
|
||||||
tun.insert("auto-detect-interface".into(), true.into());
|
tun.insert("auto-detect-interface".into(), true.into());
|
||||||
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
map.insert("redir-port".into(), 7895.into());
|
map.insert("redir-port".into(), 7895.into());
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -52,27 +46,14 @@ impl IClashTemp {
|
|||||||
map.insert("mixed-port".into(), 7897.into());
|
map.insert("mixed-port".into(), 7897.into());
|
||||||
map.insert("socks-port".into(), 7898.into());
|
map.insert("socks-port".into(), 7898.into());
|
||||||
map.insert("port".into(), 7899.into());
|
map.insert("port".into(), 7899.into());
|
||||||
map.insert("log-level".into(), "warning".into());
|
map.insert("log-level".into(), "info".into());
|
||||||
map.insert("allow-lan".into(), false.into());
|
map.insert("allow-lan".into(), false.into());
|
||||||
map.insert("ipv6".into(), true.into());
|
|
||||||
map.insert("mode".into(), "rule".into());
|
map.insert("mode".into(), "rule".into());
|
||||||
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
||||||
|
let mut cors_map = Mapping::new();
|
||||||
cors_map.insert("allow-private-network".into(), true.into());
|
cors_map.insert("allow-private-network".into(), true.into());
|
||||||
cors_map.insert(
|
cors_map.insert("allow-origins".into(), vec!["*"].into());
|
||||||
"allow-origins".into(),
|
map.insert("secret".into(), "".into());
|
||||||
vec![
|
|
||||||
"tauri://localhost",
|
|
||||||
"http://tauri.localhost",
|
|
||||||
// Only enable this in dev mode
|
|
||||||
#[cfg(feature = "verge-dev")]
|
|
||||||
"http://localhost:3000",
|
|
||||||
"https://yacd.metacubex.one",
|
|
||||||
"https://metacubex.github.io",
|
|
||||||
"https://board.zash.run.place",
|
|
||||||
]
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
map.insert("secret".into(), "set-your-secret".into());
|
|
||||||
map.insert("tun".into(), tun.into());
|
map.insert("tun".into(), tun.into());
|
||||||
map.insert("external-controller-cors".into(), cors_map.into());
|
map.insert("external-controller-cors".into(), cors_map.into());
|
||||||
map.insert("unified-delay".into(), true.into());
|
map.insert("unified-delay".into(), true.into());
|
||||||
@@ -96,26 +77,6 @@ impl IClashTemp {
|
|||||||
config.insert("socks-port".into(), socks_port.into());
|
config.insert("socks-port".into(), socks_port.into());
|
||||||
config.insert("port".into(), port.into());
|
config.insert("port".into(), port.into());
|
||||||
config.insert("external-controller".into(), ctrl.into());
|
config.insert("external-controller".into(), ctrl.into());
|
||||||
|
|
||||||
// 强制覆盖 external-controller-cors 字段,允许本地和 tauri 前端
|
|
||||||
let mut cors_map = Mapping::new();
|
|
||||||
cors_map.insert("allow-private-network".into(), true.into());
|
|
||||||
cors_map.insert(
|
|
||||||
"allow-origins".into(),
|
|
||||||
vec![
|
|
||||||
"tauri://localhost",
|
|
||||||
"http://tauri.localhost",
|
|
||||||
// Only enable this in dev mode
|
|
||||||
#[cfg(feature = "verge-dev")]
|
|
||||||
"http://localhost:3000",
|
|
||||||
"https://yacd.metacubex.one",
|
|
||||||
"https://metacubex.github.io",
|
|
||||||
"https://board.zash.run.place",
|
|
||||||
]
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
config.insert("external-controller-cors".into(), cors_map.into());
|
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,13 +316,6 @@ fn test_clash_info() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct IClashExternalControllerCors {
|
|
||||||
pub allow_origins: Option<Vec<String>>,
|
|
||||||
pub allow_private_network: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct IClash {
|
pub struct IClash {
|
||||||
@@ -375,7 +329,6 @@ pub struct IClash {
|
|||||||
pub dns: Option<IClashDNS>,
|
pub dns: Option<IClashDNS>,
|
||||||
pub tun: Option<IClashTUN>,
|
pub tun: Option<IClashTUN>,
|
||||||
pub interface_name: Option<String>,
|
pub interface_name: Option<String>,
|
||||||
pub external_controller_cors: Option<IClashExternalControllerCors>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::PrfItem,
|
config::PrfItem,
|
||||||
|
enhance,
|
||||||
|
utils::{dirs, help},
|
||||||
core::{handle, CoreManager},
|
core::{handle, CoreManager},
|
||||||
enhance, logging,
|
|
||||||
process::AsyncHandler,
|
|
||||||
utils::{dirs, help, logging::Type},
|
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@@ -15,10 +14,10 @@ pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
|||||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
clash_config: Draft<Box<IClashTemp>>,
|
clash_config: Draft<IClashTemp>,
|
||||||
verge_config: Draft<Box<IVerge>>,
|
verge_config: Draft<IVerge>,
|
||||||
profiles_config: Draft<Box<IProfiles>>,
|
profiles_config: Draft<IProfiles>,
|
||||||
runtime_config: Draft<Box<IRuntime>>,
|
runtime_config: Draft<IRuntime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -26,26 +25,26 @@ impl Config {
|
|||||||
static CONFIG: OnceCell<Config> = OnceCell::new();
|
static CONFIG: OnceCell<Config> = OnceCell::new();
|
||||||
|
|
||||||
CONFIG.get_or_init(|| Config {
|
CONFIG.get_or_init(|| Config {
|
||||||
clash_config: Draft::from(Box::new(IClashTemp::new())),
|
clash_config: Draft::from(IClashTemp::new()),
|
||||||
verge_config: Draft::from(Box::new(IVerge::new())),
|
verge_config: Draft::from(IVerge::new()),
|
||||||
profiles_config: Draft::from(Box::new(IProfiles::new())),
|
profiles_config: Draft::from(IProfiles::new()),
|
||||||
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
runtime_config: Draft::from(IRuntime::new()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clash() -> Draft<Box<IClashTemp>> {
|
pub fn clash() -> Draft<IClashTemp> {
|
||||||
Self::global().clash_config.clone()
|
Self::global().clash_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verge() -> Draft<Box<IVerge>> {
|
pub fn verge() -> Draft<IVerge> {
|
||||||
Self::global().verge_config.clone()
|
Self::global().verge_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profiles() -> Draft<Box<IProfiles>> {
|
pub fn profiles() -> Draft<IProfiles> {
|
||||||
Self::global().profiles_config.clone()
|
Self::global().profiles_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runtime() -> Draft<Box<IRuntime>> {
|
pub fn runtime() -> Draft<IRuntime> {
|
||||||
Self::global().runtime_config.clone()
|
Self::global().runtime_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,41 +66,32 @@ impl Config {
|
|||||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||||
Self::profiles().data().append_item(script_item.clone())?;
|
Self::profiles().data().append_item(script_item.clone())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成运行时配置
|
// 生成运行时配置
|
||||||
if let Err(err) = Self::generate().await {
|
crate::log_err!(Self::generate().await);
|
||||||
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
|
|
||||||
} else {
|
|
||||||
logging!(info, Type::Config, true, "生成运行时配置成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成运行时配置文件并验证
|
// 生成运行时配置文件并验证
|
||||||
let config_result = Self::generate_file(ConfigType::Run);
|
let config_result = Self::generate_file(ConfigType::Run);
|
||||||
|
|
||||||
let validation_result = if config_result.is_ok() {
|
let validation_result = if let Ok(_) = config_result {
|
||||||
// 验证配置文件
|
// 验证配置文件
|
||||||
logging!(info, Type::Config, true, "开始验证配置");
|
println!("[首次启动] 开始验证配置");
|
||||||
|
|
||||||
match CoreManager::global().validate_config().await {
|
match CoreManager::global().validate_config().await {
|
||||||
Ok((is_valid, error_msg)) => {
|
Ok((is_valid, error_msg)) => {
|
||||||
if !is_valid {
|
if !is_valid {
|
||||||
logging!(
|
println!("[首次启动] 配置验证失败,使用默认最小配置启动: {}", error_msg);
|
||||||
warn,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
|
||||||
error_msg
|
|
||||||
);
|
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::boot_error", &error_msg)
|
.use_default_config("config_validate::boot_error", &error_msg)
|
||||||
.await?;
|
.await?;
|
||||||
Some(("config_validate::boot_error", error_msg))
|
Some(("config_validate::boot_error", error_msg))
|
||||||
} else {
|
} else {
|
||||||
logging!(info, Type::Config, true, "配置验证成功");
|
println!("[首次启动] 配置验证成功");
|
||||||
Some(("config_validate::success", String::new()))
|
Some(("config_validate::success", String::new()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
|
println!("[首次启动] 验证进程执行失败: {}", err);
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::process_terminated", "")
|
.use_default_config("config_validate::process_terminated", "")
|
||||||
.await?;
|
.await?;
|
||||||
@@ -109,16 +99,19 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
println!("[首次启动] 生成配置文件失败,使用默认配置");
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::error", "")
|
.use_default_config(
|
||||||
|
"config_validate::error",
|
||||||
|
"",
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Some(("config_validate::error", String::new()))
|
Some(("config_validate::error", String::new()))
|
||||||
};
|
};
|
||||||
|
|
||||||
// 在单独的任务中发送通知
|
// 在单独的任务中发送通知
|
||||||
if let Some((msg_type, msg_content)) = validation_result {
|
if let Some((msg_type, msg_content)) = validation_result {
|
||||||
AsyncHandler::spawn(move || async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
sleep(Duration::from_secs(2)).await;
|
sleep(Duration::from_secs(2)).await;
|
||||||
handle::Handle::notice_message(msg_type, &msg_content);
|
handle::Handle::notice_message(msg_type, &msg_content);
|
||||||
});
|
});
|
||||||
@@ -149,11 +142,11 @@ impl Config {
|
|||||||
pub async fn generate() -> Result<()> {
|
pub async fn generate() -> Result<()> {
|
||||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||||
|
|
||||||
*Config::runtime().draft() = Box::new(IRuntime {
|
*Config::runtime().draft() = IRuntime {
|
||||||
config: Some(config),
|
config: Some(config),
|
||||||
exists_keys,
|
exists_keys,
|
||||||
chain_logs: logs,
|
chain_logs: logs,
|
||||||
});
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -164,42 +157,3 @@ pub enum ConfigType {
|
|||||||
Run,
|
Run,
|
||||||
Check,
|
Check,
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_prfitem_from_merge_size() {
|
|
||||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap();
|
|
||||||
dbg!(&merge_item);
|
|
||||||
let prfitem_size = mem::size_of_val(&merge_item);
|
|
||||||
dbg!(prfitem_size);
|
|
||||||
// Boxed version
|
|
||||||
let boxed_merge_item = Box::new(merge_item);
|
|
||||||
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
|
|
||||||
dbg!(box_prfitem_size);
|
|
||||||
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
|
|
||||||
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
|
|
||||||
assert!(box_prfitem_size < prfitem_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_draft_size_non_boxed() {
|
|
||||||
let draft = Draft::from(IRuntime::new());
|
|
||||||
let iruntime_size = std::mem::size_of_val(&draft);
|
|
||||||
dbg!(iruntime_size);
|
|
||||||
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_draft_size_boxed() {
|
|
||||||
let draft = Draft::from(Box::new(IRuntime::new()));
|
|
||||||
let box_iruntime_size = std::mem::size_of_val(&draft);
|
|
||||||
dbg!(box_iruntime_size);
|
|
||||||
assert_eq!(
|
|
||||||
box_iruntime_size,
|
|
||||||
std::mem::size_of::<Draft<Box<IRuntime>>>()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,21 +9,13 @@ pub struct Draft<T: Clone + ToOwned> {
|
|||||||
|
|
||||||
macro_rules! draft_define {
|
macro_rules! draft_define {
|
||||||
($id: ident) => {
|
($id: ident) => {
|
||||||
impl From<$id> for Draft<$id> {
|
impl Draft<$id> {
|
||||||
fn from(data: $id) -> Self {
|
|
||||||
Draft {
|
|
||||||
inner: Arc::new(Mutex::new((data, None))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draft<Box<$id>> {
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
|
pub fn data(&self) -> MappedMutexGuard<$id> {
|
||||||
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
|
pub fn latest(&self) -> MappedMutexGuard<$id> {
|
||||||
MutexGuard::map(self.inner.lock(), |inner| {
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
if inner.1.is_none() {
|
if inner.1.is_none() {
|
||||||
&mut inner.0
|
&mut inner.0
|
||||||
@@ -33,7 +25,7 @@ macro_rules! draft_define {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
|
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
||||||
MutexGuard::map(self.inner.lock(), |inner| {
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
if inner.1.is_none() {
|
if inner.1.is_none() {
|
||||||
inner.1 = Some(inner.0.clone());
|
inner.1 = Some(inner.0.clone());
|
||||||
@@ -43,7 +35,7 @@ macro_rules! draft_define {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply(&self) -> Option<Box<$id>> {
|
pub fn apply(&self) -> Option<$id> {
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
|
|
||||||
match inner.1.take() {
|
match inner.1.take() {
|
||||||
@@ -56,14 +48,14 @@ macro_rules! draft_define {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn discard(&self) -> Option<Box<$id>> {
|
pub fn discard(&self) -> Option<$id> {
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
inner.1.take()
|
inner.1.take()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Box<$id>> for Draft<Box<$id>> {
|
impl From<$id> for Draft<$id> {
|
||||||
fn from(data: Box<$id>) -> Self {
|
fn from(data: $id) -> Self {
|
||||||
Draft {
|
Draft {
|
||||||
inner: Arc::new(Mutex::new((data, None))),
|
inner: Arc::new(Mutex::new((data, None))),
|
||||||
}
|
}
|
||||||
@@ -79,12 +71,12 @@ draft_define!(IRuntime);
|
|||||||
draft_define!(IVerge);
|
draft_define!(IVerge);
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_draft_box() {
|
fn test_draft() {
|
||||||
let verge = Box::new(IVerge {
|
let verge = IVerge {
|
||||||
enable_auto_launch: Some(true),
|
enable_auto_launch: Some(true),
|
||||||
enable_tun_mode: Some(false),
|
enable_tun_mode: Some(false),
|
||||||
..IVerge::default()
|
..IVerge::default()
|
||||||
});
|
};
|
||||||
|
|
||||||
let draft = Draft::from(verge);
|
let draft = Draft::from(verge);
|
||||||
|
|
||||||
@@ -94,11 +86,10 @@ fn test_draft_box() {
|
|||||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
{
|
let mut d = draft.draft();
|
||||||
let mut d = draft.draft();
|
d.enable_auto_launch = Some(false);
|
||||||
d.enable_auto_launch = Some(false);
|
d.enable_tun_mode = Some(true);
|
||||||
d.enable_tun_mode = Some(true);
|
drop(d);
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||||
@@ -118,17 +109,18 @@ fn test_draft_box() {
|
|||||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
{
|
let mut d = draft.draft();
|
||||||
let mut d = draft.draft();
|
d.enable_auto_launch = Some(true);
|
||||||
d.enable_auto_launch = Some(true);
|
drop(d);
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
|
|
||||||
assert!(draft.discard().is_some());
|
assert!(draft.discard().is_some());
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
assert!(draft.discard().is_none());
|
assert!(draft.discard().is_none());
|
||||||
|
|
||||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|||||||
// Encrypt data
|
// Encrypt data
|
||||||
let ciphertext = cipher
|
let ciphertext = cipher
|
||||||
.encrypt(nonce.as_slice().into(), data.as_bytes())
|
.encrypt(nonce.as_slice().into(), data.as_bytes())
|
||||||
.map_err(|e| format!("Encryption failed: {e}"))?;
|
.map_err(|e| format!("Encryption failed: {}", e))?;
|
||||||
|
|
||||||
// Concatenate nonce and ciphertext and encode them in base64
|
// Concatenate nonce and ciphertext and encode them in base64
|
||||||
let mut combined = nonce;
|
let mut combined = nonce;
|
||||||
@@ -46,7 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
|
|||||||
// Decrypt data
|
// Decrypt data
|
||||||
let plaintext = cipher
|
let plaintext = cipher
|
||||||
.decrypt(nonce.into(), ciphertext)
|
.decrypt(nonce.into(), ciphertext)
|
||||||
.map_err(|e| format!("Decryption failed: {e}"))?;
|
.map_err(|e| format!("Decryption failed: {}", e))?;
|
||||||
|
|
||||||
String::from_utf8(plaintext).map_err(|e| e.into())
|
String::from_utf8(plaintext).map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ mod profiles;
|
|||||||
mod runtime;
|
mod runtime;
|
||||||
mod verge;
|
mod verge;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::clash::*;
|
||||||
clash::*, config::*, draft::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*,
|
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) {
|
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;";
|
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
use crate::utils::{
|
use crate::utils::{dirs, help, resolve::VERSION, tmpl};
|
||||||
dirs, help,
|
|
||||||
network::{NetworkManager, ProxyType},
|
|
||||||
tmpl,
|
|
||||||
};
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::{fs, time::Duration};
|
use std::fs;
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
use sysproxy::Sysproxy;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use super::Config;
|
use super::Config;
|
||||||
|
|
||||||
@@ -55,18 +50,6 @@ pub struct PrfItem {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub home: Option<String>,
|
pub home: Option<String>,
|
||||||
|
|
||||||
/// profile support url
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub support_url: Option<String>,
|
|
||||||
|
|
||||||
/// profile announce
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub announce: Option<String>,
|
|
||||||
|
|
||||||
/// profile announce url
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub announce_url: Option<String>,
|
|
||||||
|
|
||||||
/// the file data
|
/// the file data
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub file_data: Option<String>,
|
pub file_data: Option<String>,
|
||||||
@@ -106,12 +89,6 @@ pub struct PrfOption {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub update_interval: Option<u64>,
|
pub update_interval: Option<u64>,
|
||||||
|
|
||||||
/// for `remote` profile
|
|
||||||
/// HTTP request timeout in seconds
|
|
||||||
/// default is 60 seconds
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub timeout_seconds: Option<u64>,
|
|
||||||
|
|
||||||
/// for `remote` profile
|
/// for `remote` profile
|
||||||
/// disable certificate validation
|
/// disable certificate validation
|
||||||
/// default is `false`
|
/// default is `false`
|
||||||
@@ -127,12 +104,6 @@ pub struct PrfOption {
|
|||||||
pub proxies: Option<String>,
|
pub proxies: Option<String>,
|
||||||
|
|
||||||
pub groups: Option<String>,
|
pub groups: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub use_hwid: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub update_always: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrfOption {
|
impl PrfOption {
|
||||||
@@ -151,9 +122,6 @@ impl PrfOption {
|
|||||||
a.rules = b.rules.or(a.rules);
|
a.rules = b.rules.or(a.rules);
|
||||||
a.proxies = b.proxies.or(a.proxies);
|
a.proxies = b.proxies.or(a.proxies);
|
||||||
a.groups = b.groups.or(a.groups);
|
a.groups = b.groups.or(a.groups);
|
||||||
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
|
|
||||||
a.use_hwid = b.use_hwid.or(a.use_hwid);
|
|
||||||
a.update_always = b.update_always.or(a.update_always);
|
|
||||||
Some(a)
|
Some(a)
|
||||||
}
|
}
|
||||||
t => t.0.or(t.1),
|
t => t.0.or(t.1),
|
||||||
@@ -252,9 +220,6 @@ impl PrfItem {
|
|||||||
..PrfOption::default()
|
..PrfOption::default()
|
||||||
}),
|
}),
|
||||||
home: None,
|
home: None,
|
||||||
support_url: None,
|
|
||||||
announce: None,
|
|
||||||
announce_url: None,
|
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||||
})
|
})
|
||||||
@@ -269,48 +234,65 @@ impl PrfItem {
|
|||||||
option: Option<PrfOption>,
|
option: Option<PrfOption>,
|
||||||
) -> Result<PrfItem> {
|
) -> Result<PrfItem> {
|
||||||
let opt_ref = option.as_ref();
|
let opt_ref = option.as_ref();
|
||||||
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
||||||
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
||||||
let accept_invalid_certs =
|
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 user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||||
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
|
|
||||||
let use_hwid = Config::verge().latest().enable_send_hwid.unwrap_or(true);
|
|
||||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||||
let mut script = opt_ref.and_then(|o| o.script.clone());
|
let mut script = opt_ref.and_then(|o| o.script.clone());
|
||||||
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
||||||
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
||||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||||
|
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||||
|
|
||||||
// 选择代理类型
|
// 使用软件自己的代理
|
||||||
let proxy_type = if self_proxy {
|
if self_proxy {
|
||||||
ProxyType::Localhost
|
let port = Config::verge()
|
||||||
} else if with_proxy {
|
.latest()
|
||||||
ProxyType::System
|
.verge_mixed_port
|
||||||
} else {
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
ProxyType::None
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用网络管理器发送请求
|
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||||
let resp = match NetworkManager::global()
|
|
||||||
.get_with_interrupt(
|
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||||
url,
|
builder = builder.proxy(proxy);
|
||||||
proxy_type,
|
|
||||||
Some(timeout),
|
|
||||||
user_agent.clone(),
|
|
||||||
accept_invalid_certs,
|
|
||||||
use_hwid,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(e) => {
|
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
||||||
bail!("failed to fetch remote profile: {}", e);
|
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 使用系统代理
|
||||||
|
else if with_proxy {
|
||||||
|
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
|
||||||
|
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
||||||
|
|
||||||
|
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 version = match VERSION.get() {
|
||||||
|
Some(v) => format!("clash-verge/v{}", v),
|
||||||
|
None => "clash-verge/unknown".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
builder = builder.danger_accept_invalid_certs(accept_invalid_certs);
|
||||||
|
builder = builder.user_agent(user_agent.unwrap_or(version));
|
||||||
|
|
||||||
|
let resp = builder.build()?.get(url).send().await?;
|
||||||
|
|
||||||
let status_code = resp.status();
|
let status_code = resp.status();
|
||||||
if !StatusCode::is_success(&status_code) {
|
if !StatusCode::is_success(&status_code) {
|
||||||
bail!("failed to fetch remote profile with status {status_code}")
|
bail!("failed to fetch remote profile with status {status_code}")
|
||||||
@@ -318,21 +300,6 @@ impl PrfItem {
|
|||||||
|
|
||||||
let header = resp.headers();
|
let header = resp.headers();
|
||||||
|
|
||||||
let mut final_url = url.to_string();
|
|
||||||
|
|
||||||
if let Some(new_domain_value) = header.get("new-sub-domain") {
|
|
||||||
if let Ok(new_domain) = new_domain_value.to_str() {
|
|
||||||
if !new_domain.is_empty() {
|
|
||||||
if let Ok(mut parsed_url) = Url::parse(url) {
|
|
||||||
if parsed_url.set_host(Some(new_domain)).is_ok() {
|
|
||||||
final_url = parsed_url.to_string();
|
|
||||||
log::info!(target: "app", "URL host updated to -> {}", final_url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse the Subscription UserInfo
|
// parse the Subscription UserInfo
|
||||||
let extra = match header.get("Subscription-Userinfo") {
|
let extra = match header.get("Subscription-Userinfo") {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
@@ -382,11 +349,6 @@ impl PrfItem {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let update_always = match header.get("update-always") {
|
|
||||||
Some(value) => value.to_str().unwrap_or("false").parse::<bool>().ok(),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let home = match header.get("profile-web-page-url") {
|
let home = match header.get("profile-web-page-url") {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
let str_value = value.to_str().unwrap_or("");
|
let str_value = value.to_str().unwrap_or("");
|
||||||
@@ -395,53 +357,9 @@ impl PrfItem {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let support_url = match header.get("support-url") {
|
|
||||||
Some(value) => {
|
|
||||||
let str_value = value.to_str().unwrap_or("");
|
|
||||||
Some(str_value.to_string())
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let announce = match header.get("announce") {
|
|
||||||
Some(value) => {
|
|
||||||
let str_value = value.to_str().unwrap_or("");
|
|
||||||
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
|
||||||
STANDARD.decode(b64_data)
|
|
||||||
.ok()
|
|
||||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
|
||||||
} else {
|
|
||||||
Some(str_value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let announce_url = match header.get("announce-url") {
|
|
||||||
Some(value) => {
|
|
||||||
let str_value = value.to_str().unwrap_or("");
|
|
||||||
Some(str_value.to_string())
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let profile_title = match header.get("profile-title") {
|
|
||||||
Some(value) => {
|
|
||||||
let str_value = value.to_str().unwrap_or("");
|
|
||||||
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
|
||||||
STANDARD.decode(b64_data)
|
|
||||||
.ok()
|
|
||||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
|
||||||
} else {
|
|
||||||
Some(str_value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let uid = help::get_uid("R");
|
let uid = help::get_uid("R");
|
||||||
let file = format!("{uid}.yaml");
|
let file = format!("{uid}.yaml");
|
||||||
let name = name.or(profile_title).unwrap_or(filename.unwrap_or("Remote File".into()));
|
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||||
let data = resp.text_with_charset("utf-8").await?;
|
let data = resp.text_with_charset("utf-8").await?;
|
||||||
|
|
||||||
// process the charset "UTF-8 with BOM"
|
// process the charset "UTF-8 with BOM"
|
||||||
@@ -489,12 +407,11 @@ impl PrfItem {
|
|||||||
name: Some(name),
|
name: Some(name),
|
||||||
desc,
|
desc,
|
||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: Some(final_url),
|
url: Some(url.into()),
|
||||||
selected: None,
|
selected: None,
|
||||||
extra,
|
extra,
|
||||||
option: Some(PrfOption {
|
option: Some(PrfOption {
|
||||||
update_interval,
|
update_interval,
|
||||||
update_always,
|
|
||||||
merge,
|
merge,
|
||||||
script,
|
script,
|
||||||
rules,
|
rules,
|
||||||
@@ -503,9 +420,6 @@ impl PrfItem {
|
|||||||
..PrfOption::default()
|
..PrfOption::default()
|
||||||
}),
|
}),
|
||||||
home,
|
home,
|
||||||
support_url,
|
|
||||||
announce,
|
|
||||||
announce_url,
|
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(data.into()),
|
file_data: Some(data.into()),
|
||||||
})
|
})
|
||||||
@@ -533,9 +447,6 @@ impl PrfItem {
|
|||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
home: None,
|
home: None,
|
||||||
support_url: None,
|
|
||||||
announce: None,
|
|
||||||
announce_url: None,
|
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(template),
|
file_data: Some(template),
|
||||||
})
|
})
|
||||||
@@ -558,9 +469,6 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
support_url: None,
|
|
||||||
announce: None,
|
|
||||||
announce_url: None,
|
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
@@ -582,9 +490,6 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
support_url: None,
|
|
||||||
announce: None,
|
|
||||||
announce_url: None,
|
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
@@ -606,9 +511,6 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
support_url: None,
|
|
||||||
announce: None,
|
|
||||||
announce_url: None,
|
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
@@ -630,9 +532,6 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
support_url: None,
|
|
||||||
announce: None,
|
|
||||||
announce_url: None,
|
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::utils::{dirs, help};
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::{collections::HashSet, fs, io::Write};
|
use std::{fs, io::Write};
|
||||||
|
|
||||||
/// Define the `profiles.yaml` schema
|
/// Define the `profiles.yaml` schema
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -15,14 +15,6 @@ pub struct IProfiles {
|
|||||||
pub items: Option<Vec<PrfItem>>,
|
pub items: Option<Vec<PrfItem>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清理结果
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CleanupResult {
|
|
||||||
pub total_files: usize,
|
|
||||||
pub deleted_files: Vec<String>,
|
|
||||||
pub failed_deletions: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! patch {
|
macro_rules! patch {
|
||||||
($lv: expr, $rv: expr, $key: tt) => {
|
($lv: expr, $rv: expr, $key: tt) => {
|
||||||
if ($rv.$key).is_some() {
|
if ($rv.$key).is_some() {
|
||||||
@@ -131,9 +123,9 @@ impl IProfiles {
|
|||||||
let path = dirs::app_profiles_dir()?.join(&file);
|
let path = dirs::app_profiles_dir()?.join(&file);
|
||||||
|
|
||||||
fs::File::create(path)
|
fs::File::create(path)
|
||||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||||
.write(file_data.as_bytes())
|
.write(file_data.as_bytes())
|
||||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.current.is_none()
|
if self.current.is_none()
|
||||||
@@ -220,11 +212,6 @@ impl IProfiles {
|
|||||||
each.extra = item.extra;
|
each.extra = item.extra;
|
||||||
each.updated = item.updated;
|
each.updated = item.updated;
|
||||||
each.home = item.home;
|
each.home = item.home;
|
||||||
each.announce = item.announce;
|
|
||||||
each.announce_url = item.announce_url;
|
|
||||||
each.support_url = item.support_url;
|
|
||||||
each.name = item.name;
|
|
||||||
each.url = item.url;
|
|
||||||
each.option = PrfOption::merge(each.option.clone(), item.option);
|
each.option = PrfOption::merge(each.option.clone(), item.option);
|
||||||
// save the file data
|
// save the file data
|
||||||
// move the field value after save
|
// move the field value after save
|
||||||
@@ -239,9 +226,9 @@ impl IProfiles {
|
|||||||
let path = dirs::app_profiles_dir()?.join(&file);
|
let path = dirs::app_profiles_dir()?.join(&file);
|
||||||
|
|
||||||
fs::File::create(path)
|
fs::File::create(path)
|
||||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||||
.write(file_data.as_bytes())
|
.write(file_data.as_bytes())
|
||||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -485,210 +472,15 @@ impl IProfiles {
|
|||||||
|
|
||||||
/// 获取所有的profiles(uid,名称)
|
/// 获取所有的profiles(uid,名称)
|
||||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
||||||
self.items.as_ref().map(|items| {
|
match self.items.as_ref() {
|
||||||
items
|
Some(items) => Some(items.iter().filter_map(|e| {
|
||||||
.iter()
|
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
||||||
.filter_map(|e| {
|
Some((uid, name))
|
||||||
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
} else {
|
||||||
Some((uid, name))
|
None
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 以 app 中的 profile 列表为准,删除不再需要的文件
|
|
||||||
pub fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
|
|
||||||
let profiles_dir = dirs::app_profiles_dir()?;
|
|
||||||
|
|
||||||
if !profiles_dir.exists() {
|
|
||||||
return Ok(CleanupResult {
|
|
||||||
total_files: 0,
|
|
||||||
deleted_files: vec![],
|
|
||||||
failed_deletions: vec![],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有 active profile 的文件名集合
|
|
||||||
let active_files = self.get_all_active_files();
|
|
||||||
|
|
||||||
// 添加全局扩展配置文件到保护列表
|
|
||||||
let protected_files = self.get_protected_global_files();
|
|
||||||
|
|
||||||
// 扫描 profiles 目录下的所有文件
|
|
||||||
let mut total_files = 0;
|
|
||||||
let mut deleted_files = vec![];
|
|
||||||
let mut failed_deletions = vec![];
|
|
||||||
|
|
||||||
for entry in std::fs::read_dir(&profiles_dir)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
if !path.is_file() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
total_files += 1;
|
|
||||||
|
|
||||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
|
||||||
if Self::is_profile_file(file_name) {
|
|
||||||
// 检查是否为全局扩展文件
|
|
||||||
if protected_files.contains(file_name) {
|
|
||||||
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为活跃文件
|
|
||||||
if !active_files.contains(file_name) {
|
|
||||||
match std::fs::remove_file(&path) {
|
|
||||||
Ok(_) => {
|
|
||||||
deleted_files.push(file_name.to_string());
|
|
||||||
log::info!(target: "app", "已清理冗余文件: {file_name}");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
failed_deletions.push(format!("{file_name}: {e}"));
|
|
||||||
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}).collect()),
|
||||||
}
|
None => None,
|
||||||
|
|
||||||
let result = CleanupResult {
|
|
||||||
total_files,
|
|
||||||
deleted_files,
|
|
||||||
failed_deletions,
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
target: "app",
|
|
||||||
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
|
|
||||||
result.total_files,
|
|
||||||
result.deleted_files.len(),
|
|
||||||
result.failed_deletions.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 不删除全局扩展配置
|
|
||||||
fn get_protected_global_files(&self) -> HashSet<String> {
|
|
||||||
let mut protected_files = HashSet::new();
|
|
||||||
|
|
||||||
protected_files.insert("Merge.yaml".to_string());
|
|
||||||
protected_files.insert("Script.js".to_string());
|
|
||||||
|
|
||||||
protected_files
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取所有 active profile 关联的文件名
|
|
||||||
fn get_all_active_files(&self) -> HashSet<String> {
|
|
||||||
let mut active_files = HashSet::new();
|
|
||||||
|
|
||||||
if let Some(items) = &self.items {
|
|
||||||
for item in items {
|
|
||||||
// 收集所有类型 profile 的文件
|
|
||||||
if let Some(file) = &item.file {
|
|
||||||
active_files.insert(file.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
|
|
||||||
if let Some(itype) = &item.itype {
|
|
||||||
if itype == "remote" || itype == "local" {
|
|
||||||
if let Some(option) = &item.option {
|
|
||||||
// 收集关联的扩展文件
|
|
||||||
if let Some(merge_uid) = &option.merge {
|
|
||||||
if let Ok(merge_item) = self.get_item(merge_uid) {
|
|
||||||
if let Some(file) = &merge_item.file {
|
|
||||||
active_files.insert(file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(script_uid) = &option.script {
|
|
||||||
if let Ok(script_item) = self.get_item(script_uid) {
|
|
||||||
if let Some(file) = &script_item.file {
|
|
||||||
active_files.insert(file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(rules_uid) = &option.rules {
|
|
||||||
if let Ok(rules_item) = self.get_item(rules_uid) {
|
|
||||||
if let Some(file) = &rules_item.file {
|
|
||||||
active_files.insert(file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(proxies_uid) = &option.proxies {
|
|
||||||
if let Ok(proxies_item) = self.get_item(proxies_uid) {
|
|
||||||
if let Some(file) = &proxies_item.file {
|
|
||||||
active_files.insert(file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(groups_uid) = &option.groups {
|
|
||||||
if let Ok(groups_item) = self.get_item(groups_uid) {
|
|
||||||
if let Some(file) = &groups_item.file {
|
|
||||||
active_files.insert(file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
active_files
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查文件名是否符合 profile 文件的命名规则
|
|
||||||
fn is_profile_file(filename: &str) -> bool {
|
|
||||||
// 匹配各种 profile 文件格式
|
|
||||||
// R12345678.yaml (remote)
|
|
||||||
// L12345678.yaml (local)
|
|
||||||
// m12345678.yaml (merge)
|
|
||||||
// s12345678.js (script)
|
|
||||||
// r12345678.yaml (rules)
|
|
||||||
// p12345678.yaml (proxies)
|
|
||||||
// g12345678.yaml (groups)
|
|
||||||
|
|
||||||
let patterns = [
|
|
||||||
r"^[RL][a-zA-Z0-9]+\.yaml$", // Remote/Local profiles
|
|
||||||
r"^m[a-zA-Z0-9]+\.yaml$", // Merge files
|
|
||||||
r"^s[a-zA-Z0-9]+\.js$", // Script files
|
|
||||||
r"^[rpg][a-zA-Z0-9]+\.yaml$", // Rules/Proxies/Groups files
|
|
||||||
];
|
|
||||||
|
|
||||||
patterns.iter().any(|pattern| {
|
|
||||||
regex::Regex::new(pattern)
|
|
||||||
.map(|re| re.is_match(filename))
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn auto_cleanup(&self) -> Result<()> {
|
|
||||||
match self.cleanup_orphaned_files() {
|
|
||||||
Ok(result) => {
|
|
||||||
if !result.deleted_files.is_empty() {
|
|
||||||
log::info!(
|
|
||||||
target: "app",
|
|
||||||
"自动清理完成,删除了 {} 个冗余文件",
|
|
||||||
result.deleted_files.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!(target: "app", "自动清理失败: {e}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use crate::{
|
use crate::config::DEFAULT_PAC;
|
||||||
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
|
use crate::config::{deserialize_encrypted, serialize_encrypted};
|
||||||
logging,
|
use crate::utils::i18n;
|
||||||
utils::{dirs, help, i18n, logging::Type},
|
use crate::utils::{dirs, help};
|
||||||
};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -74,10 +73,6 @@ pub struct IVerge {
|
|||||||
/// enable dns settings - this controls whether dns_config.yaml is applied
|
/// enable dns settings - this controls whether dns_config.yaml is applied
|
||||||
pub enable_dns_settings: Option<bool>,
|
pub enable_dns_settings: Option<bool>,
|
||||||
|
|
||||||
pub enable_send_hwid: Option<bool>,
|
|
||||||
|
|
||||||
pub primary_action: Option<String>,
|
|
||||||
|
|
||||||
/// always use default bypass
|
/// always use default bypass
|
||||||
pub use_default_bypass: Option<bool>,
|
pub use_default_bypass: Option<bool>,
|
||||||
|
|
||||||
@@ -93,9 +88,6 @@ pub struct IVerge {
|
|||||||
/// pac script content
|
/// pac script content
|
||||||
pub pac_file_content: Option<String>,
|
pub pac_file_content: Option<String>,
|
||||||
|
|
||||||
/// proxy host address
|
|
||||||
pub proxy_host: Option<String>,
|
|
||||||
|
|
||||||
/// theme setting
|
/// theme setting
|
||||||
pub theme_setting: Option<IVergeTheme>,
|
pub theme_setting: Option<IVergeTheme>,
|
||||||
|
|
||||||
@@ -109,14 +101,10 @@ pub struct IVerge {
|
|||||||
/// hotkey map
|
/// hotkey map
|
||||||
/// format: {func},{key}
|
/// format: {func},{key}
|
||||||
pub hotkeys: Option<Vec<String>>,
|
pub hotkeys: Option<Vec<String>>,
|
||||||
|
|
||||||
/// enable global hotkey
|
/// enable global hotkey
|
||||||
pub enable_global_hotkey: Option<bool>,
|
pub enable_global_hotkey: Option<bool>,
|
||||||
|
|
||||||
/// 首页卡片设置
|
|
||||||
/// 控制首页各个卡片的显示和隐藏
|
|
||||||
pub home_cards: Option<serde_json::Value>,
|
|
||||||
|
|
||||||
/// 切换代理时自动关闭连接
|
/// 切换代理时自动关闭连接
|
||||||
pub auto_close_connection: Option<bool>,
|
pub auto_close_connection: Option<bool>,
|
||||||
|
|
||||||
@@ -139,7 +127,7 @@ pub struct IVerge {
|
|||||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||||
|
|
||||||
/// 日志清理
|
/// 日志清理
|
||||||
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
|
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
||||||
pub auto_log_clean: Option<i32>,
|
pub auto_log_clean: Option<i32>,
|
||||||
|
|
||||||
/// 是否启用随机端口
|
/// 是否启用随机端口
|
||||||
@@ -197,19 +185,8 @@ pub struct IVerge {
|
|||||||
|
|
||||||
pub enable_tray_speed: Option<bool>,
|
pub enable_tray_speed: Option<bool>,
|
||||||
|
|
||||||
pub enable_tray_icon: Option<bool>,
|
/// 轻量模式 - 只保留内核运行
|
||||||
|
pub enable_lite_mode: Option<bool>,
|
||||||
/// 自动进入轻量模式
|
|
||||||
pub enable_auto_light_weight_mode: Option<bool>,
|
|
||||||
|
|
||||||
/// 自动进入轻量模式的延迟(分钟)
|
|
||||||
pub auto_light_weight_minutes: Option<u64>,
|
|
||||||
|
|
||||||
/// 启用代理页面自动滚动
|
|
||||||
pub enable_hover_jump_navigator: Option<bool>,
|
|
||||||
|
|
||||||
/// 服务状态跟踪
|
|
||||||
pub service_state: Option<crate::core::service::ServiceState>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -237,93 +214,6 @@ pub struct IVergeTheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IVerge {
|
impl IVerge {
|
||||||
/// 有效的clash核心名称
|
|
||||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
|
||||||
|
|
||||||
/// 验证并修正配置文件中的clash_core值
|
|
||||||
pub fn validate_and_fix_config() -> Result<()> {
|
|
||||||
let config_path = dirs::verge_path()?;
|
|
||||||
let mut config = match help::read_yaml::<IVerge>(&config_path) {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(_) => Self::template(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut needs_fix = false;
|
|
||||||
|
|
||||||
if let Some(ref core) = config.clash_core {
|
|
||||||
let core_str = core.trim();
|
|
||||||
if core_str.is_empty() || !Self::VALID_CLASH_CORES.contains(&core_str) {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
|
|
||||||
core
|
|
||||||
);
|
|
||||||
config.clash_core = Some("verge-mihomo".to_string());
|
|
||||||
needs_fix = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
|
|
||||||
);
|
|
||||||
config.clash_core = Some("verge-mihomo".to_string());
|
|
||||||
needs_fix = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修正后保存配置
|
|
||||||
if needs_fix {
|
|
||||||
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
|
|
||||||
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"配置文件修正完成,需要重新加载配置"
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::reload_config_after_fix(config)?;
|
|
||||||
} else {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"clash_core配置验证通过: {:?}",
|
|
||||||
config.clash_core
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 配置修正后重新加载配置
|
|
||||||
fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
|
|
||||||
use crate::config::Config;
|
|
||||||
|
|
||||||
let config_draft = Config::verge();
|
|
||||||
*config_draft.draft() = Box::new(updated_config.clone());
|
|
||||||
config_draft.apply();
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Config,
|
|
||||||
true,
|
|
||||||
"内存配置已强制更新,新的clash_core: {:?}",
|
|
||||||
updated_config.clash_core
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_valid_clash_core(&self) -> String {
|
|
||||||
self.clash_core
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "verge-mihomo".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_system_language() -> String {
|
fn get_system_language() -> String {
|
||||||
let sys_lang = sys_locale::get_locale()
|
let sys_lang = sys_locale::get_locale()
|
||||||
.unwrap_or_else(|| String::from("en"))
|
.unwrap_or_else(|| String::from("en"))
|
||||||
@@ -358,7 +248,7 @@ impl IVerge {
|
|||||||
env_type: Some("bash".into()),
|
env_type: Some("bash".into()),
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
env_type: Some("powershell".into()),
|
env_type: Some("powershell".into()),
|
||||||
start_page: Some("/home".into()),
|
start_page: Some("/".into()),
|
||||||
traffic_graph: Some(true),
|
traffic_graph: Some(true),
|
||||||
enable_memory_usage: Some(true),
|
enable_memory_usage: Some(true),
|
||||||
enable_group_icon: Some(true),
|
enable_group_icon: Some(true),
|
||||||
@@ -370,11 +260,9 @@ impl IVerge {
|
|||||||
tun_tray_icon: Some(false),
|
tun_tray_icon: Some(false),
|
||||||
enable_auto_launch: Some(false),
|
enable_auto_launch: Some(false),
|
||||||
enable_silent_start: Some(false),
|
enable_silent_start: Some(false),
|
||||||
enable_hover_jump_navigator: Some(true),
|
|
||||||
enable_system_proxy: Some(false),
|
enable_system_proxy: Some(false),
|
||||||
proxy_auto_config: Some(false),
|
proxy_auto_config: Some(false),
|
||||||
pac_file_content: Some(DEFAULT_PAC.into()),
|
pac_file_content: Some(DEFAULT_PAC.into()),
|
||||||
proxy_host: Some("127.0.0.1".into()),
|
|
||||||
enable_random_port: Some(false),
|
enable_random_port: Some(false),
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
verge_redir_port: Some(7895),
|
verge_redir_port: Some(7895),
|
||||||
@@ -395,20 +283,14 @@ impl IVerge {
|
|||||||
auto_close_connection: Some(true),
|
auto_close_connection: Some(true),
|
||||||
auto_check_update: Some(true),
|
auto_check_update: Some(true),
|
||||||
enable_builtin_enhanced: Some(true),
|
enable_builtin_enhanced: Some(true),
|
||||||
auto_log_clean: Some(2),
|
auto_log_clean: Some(3),
|
||||||
webdav_url: None,
|
webdav_url: None,
|
||||||
webdav_username: None,
|
webdav_username: None,
|
||||||
webdav_password: None,
|
webdav_password: None,
|
||||||
enable_tray_speed: Some(false),
|
enable_tray_speed: Some(true),
|
||||||
enable_tray_icon: Some(true),
|
|
||||||
enable_global_hotkey: Some(true),
|
enable_global_hotkey: Some(true),
|
||||||
enable_auto_light_weight_mode: Some(false),
|
enable_lite_mode: Some(false),
|
||||||
auto_light_weight_minutes: Some(10),
|
enable_dns_settings: Some(true),
|
||||||
enable_dns_settings: Some(false),
|
|
||||||
enable_send_hwid: Some(true),
|
|
||||||
primary_action: Some("tun-mode".into()),
|
|
||||||
home_cards: None,
|
|
||||||
service_state: None,
|
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,7 +331,6 @@ impl IVerge {
|
|||||||
patch!(enable_tun_mode);
|
patch!(enable_tun_mode);
|
||||||
patch!(enable_auto_launch);
|
patch!(enable_auto_launch);
|
||||||
patch!(enable_silent_start);
|
patch!(enable_silent_start);
|
||||||
patch!(enable_hover_jump_navigator);
|
|
||||||
patch!(enable_random_port);
|
patch!(enable_random_port);
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
patch!(verge_redir_port);
|
patch!(verge_redir_port);
|
||||||
@@ -471,7 +352,7 @@ impl IVerge {
|
|||||||
patch!(proxy_guard_duration);
|
patch!(proxy_guard_duration);
|
||||||
patch!(proxy_auto_config);
|
patch!(proxy_auto_config);
|
||||||
patch!(pac_file_content);
|
patch!(pac_file_content);
|
||||||
patch!(proxy_host);
|
|
||||||
patch!(theme_setting);
|
patch!(theme_setting);
|
||||||
patch!(web_ui_list);
|
patch!(web_ui_list);
|
||||||
patch!(clash_core);
|
patch!(clash_core);
|
||||||
@@ -491,14 +372,8 @@ impl IVerge {
|
|||||||
patch!(webdav_username);
|
patch!(webdav_username);
|
||||||
patch!(webdav_password);
|
patch!(webdav_password);
|
||||||
patch!(enable_tray_speed);
|
patch!(enable_tray_speed);
|
||||||
patch!(enable_tray_icon);
|
patch!(enable_lite_mode);
|
||||||
patch!(enable_auto_light_weight_mode);
|
|
||||||
patch!(auto_light_weight_minutes);
|
|
||||||
patch!(enable_dns_settings);
|
patch!(enable_dns_settings);
|
||||||
patch!(enable_send_hwid);
|
|
||||||
patch!(primary_action);
|
|
||||||
patch!(home_cards);
|
|
||||||
patch!(service_state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在初始化前尝试拿到单例端口的值
|
/// 在初始化前尝试拿到单例端口的值
|
||||||
@@ -557,7 +432,6 @@ pub struct IVergeResponse {
|
|||||||
pub proxy_guard_duration: Option<u64>,
|
pub proxy_guard_duration: Option<u64>,
|
||||||
pub proxy_auto_config: Option<bool>,
|
pub proxy_auto_config: Option<bool>,
|
||||||
pub pac_file_content: Option<String>,
|
pub pac_file_content: Option<String>,
|
||||||
pub proxy_host: Option<String>,
|
|
||||||
pub theme_setting: Option<IVergeTheme>,
|
pub theme_setting: Option<IVergeTheme>,
|
||||||
pub web_ui_list: Option<Vec<String>>,
|
pub web_ui_list: Option<Vec<String>>,
|
||||||
pub clash_core: Option<String>,
|
pub clash_core: Option<String>,
|
||||||
@@ -588,21 +462,12 @@ pub struct IVergeResponse {
|
|||||||
pub webdav_username: Option<String>,
|
pub webdav_username: Option<String>,
|
||||||
pub webdav_password: Option<String>,
|
pub webdav_password: Option<String>,
|
||||||
pub enable_tray_speed: Option<bool>,
|
pub enable_tray_speed: Option<bool>,
|
||||||
pub enable_tray_icon: Option<bool>,
|
pub enable_lite_mode: Option<bool>,
|
||||||
pub enable_auto_light_weight_mode: Option<bool>,
|
|
||||||
pub auto_light_weight_minutes: Option<u64>,
|
|
||||||
pub enable_dns_settings: Option<bool>,
|
pub enable_dns_settings: Option<bool>,
|
||||||
pub enable_send_hwid: Option<bool>,
|
|
||||||
pub primary_action: Option<String>,
|
|
||||||
pub home_cards: Option<serde_json::Value>,
|
|
||||||
pub enable_hover_jump_navigator: Option<bool>,
|
|
||||||
pub service_state: Option<crate::core::service::ServiceState>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<IVerge> for IVergeResponse {
|
impl From<IVerge> for IVergeResponse {
|
||||||
fn from(verge: IVerge) -> Self {
|
fn from(verge: IVerge) -> Self {
|
||||||
// 先获取验证后的clash_core值,避免后续借用冲突
|
|
||||||
let valid_clash_core = verge.get_valid_clash_core();
|
|
||||||
Self {
|
Self {
|
||||||
app_log_level: verge.app_log_level,
|
app_log_level: verge.app_log_level,
|
||||||
language: verge.language,
|
language: verge.language,
|
||||||
@@ -631,10 +496,9 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
proxy_guard_duration: verge.proxy_guard_duration,
|
proxy_guard_duration: verge.proxy_guard_duration,
|
||||||
proxy_auto_config: verge.proxy_auto_config,
|
proxy_auto_config: verge.proxy_auto_config,
|
||||||
pac_file_content: verge.pac_file_content,
|
pac_file_content: verge.pac_file_content,
|
||||||
proxy_host: verge.proxy_host,
|
|
||||||
theme_setting: verge.theme_setting,
|
theme_setting: verge.theme_setting,
|
||||||
web_ui_list: verge.web_ui_list,
|
web_ui_list: verge.web_ui_list,
|
||||||
clash_core: Some(valid_clash_core),
|
clash_core: verge.clash_core,
|
||||||
hotkeys: verge.hotkeys,
|
hotkeys: verge.hotkeys,
|
||||||
auto_close_connection: verge.auto_close_connection,
|
auto_close_connection: verge.auto_close_connection,
|
||||||
auto_check_update: verge.auto_check_update,
|
auto_check_update: verge.auto_check_update,
|
||||||
@@ -662,15 +526,8 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
webdav_username: verge.webdav_username,
|
webdav_username: verge.webdav_username,
|
||||||
webdav_password: verge.webdav_password,
|
webdav_password: verge.webdav_password,
|
||||||
enable_tray_speed: verge.enable_tray_speed,
|
enable_tray_speed: verge.enable_tray_speed,
|
||||||
enable_tray_icon: verge.enable_tray_icon,
|
enable_lite_mode: verge.enable_lite_mode,
|
||||||
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,
|
enable_dns_settings: verge.enable_dns_settings,
|
||||||
enable_send_hwid: verge.enable_send_hwid,
|
|
||||||
primary_action: verge.primary_action,
|
|
||||||
home_cards: verge.home_cards,
|
|
||||||
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
|
||||||
service_state: verge.service_state,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,531 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::time::{timeout, Duration};
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use anyhow::anyhow;
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
use tokio::process::Command;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct AsyncAutoproxy {
|
|
||||||
pub enable: bool,
|
|
||||||
pub url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct AsyncSysproxy {
|
|
||||||
pub enable: bool,
|
|
||||||
pub host: String,
|
|
||||||
pub port: u16,
|
|
||||||
pub bypass: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AsyncSysproxy {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
enable: false,
|
|
||||||
host: "127.0.0.1".to_string(),
|
|
||||||
port: 7890,
|
|
||||||
bypass: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AsyncProxyQuery;
|
|
||||||
|
|
||||||
impl AsyncProxyQuery {
|
|
||||||
/// 异步获取自动代理配置(PAC)
|
|
||||||
pub async fn get_auto_proxy() -> AsyncAutoproxy {
|
|
||||||
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
|
|
||||||
Ok(Ok(proxy)) => {
|
|
||||||
log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url);
|
|
||||||
proxy
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
log::warn!(target: "app", "异步获取自动代理失败: {e}");
|
|
||||||
AsyncAutoproxy::default()
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
log::warn!(target: "app", "异步获取自动代理超时");
|
|
||||||
AsyncAutoproxy::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 异步获取系统代理配置
|
|
||||||
pub async fn get_system_proxy() -> AsyncSysproxy {
|
|
||||||
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
|
|
||||||
Ok(Ok(proxy)) => {
|
|
||||||
log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
|
|
||||||
proxy
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
log::warn!(target: "app", "异步获取系统代理失败: {e}");
|
|
||||||
AsyncSysproxy::default()
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
log::warn!(target: "app", "异步获取系统代理超时");
|
|
||||||
AsyncSysproxy::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
|
||||||
// Windows: 从注册表读取PAC配置
|
|
||||||
tokio::task::spawn_blocking(move || -> Result<AsyncAutoproxy> {
|
|
||||||
Self::get_pac_config_from_registry()
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn get_pac_config_from_registry() -> Result<AsyncAutoproxy> {
|
|
||||||
use std::ptr;
|
|
||||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
|
||||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
|
||||||
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
|
||||||
.encode_utf16()
|
|
||||||
.collect::<Vec<u16>>();
|
|
||||||
|
|
||||||
let mut hkey: HKEY = ptr::null_mut();
|
|
||||||
let result =
|
|
||||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
|
||||||
|
|
||||||
if result != 0 {
|
|
||||||
log::debug!(target: "app", "无法打开注册表项");
|
|
||||||
return Ok(AsyncAutoproxy::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用)
|
|
||||||
let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::<Vec<u16>>();
|
|
||||||
let mut url_buffer = vec![0u16; 1024];
|
|
||||||
let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD;
|
|
||||||
let mut url_value_type: DWORD = 0;
|
|
||||||
|
|
||||||
let url_query_result = RegQueryValueExW(
|
|
||||||
hkey,
|
|
||||||
auto_config_url_name.as_ptr(),
|
|
||||||
ptr::null_mut(),
|
|
||||||
&mut url_value_type,
|
|
||||||
url_buffer.as_mut_ptr() as *mut u8,
|
|
||||||
&mut url_buffer_size,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut pac_url = String::new();
|
|
||||||
if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 {
|
|
||||||
let end_pos = url_buffer
|
|
||||||
.iter()
|
|
||||||
.position(|&x| x == 0)
|
|
||||||
.unwrap_or(url_buffer.len());
|
|
||||||
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
|
|
||||||
log::debug!(target: "app", "从注册表读取到PAC URL: {}", pac_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 检查自动检测设置是否启用
|
|
||||||
let auto_detect_name = "AutoDetect\0".encode_utf16().collect::<Vec<u16>>();
|
|
||||||
let mut auto_detect: DWORD = 0;
|
|
||||||
let mut detect_buffer_size: DWORD = 4;
|
|
||||||
let mut detect_value_type: DWORD = 0;
|
|
||||||
|
|
||||||
let detect_query_result = RegQueryValueExW(
|
|
||||||
hkey,
|
|
||||||
auto_detect_name.as_ptr(),
|
|
||||||
ptr::null_mut(),
|
|
||||||
&mut detect_value_type,
|
|
||||||
&mut auto_detect as *mut DWORD as *mut u8,
|
|
||||||
&mut detect_buffer_size,
|
|
||||||
);
|
|
||||||
|
|
||||||
RegCloseKey(hkey);
|
|
||||||
|
|
||||||
// PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用
|
|
||||||
let pac_enabled = !pac_url.is_empty()
|
|
||||||
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
|
|
||||||
|
|
||||||
if pac_enabled {
|
|
||||||
log::debug!(target: "app", "PAC配置启用: URL={}, AutoDetect={}", pac_url, auto_detect);
|
|
||||||
|
|
||||||
if pac_url.is_empty() && auto_detect != 0 {
|
|
||||||
pac_url = "auto-detect".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AsyncAutoproxy {
|
|
||||||
enable: true,
|
|
||||||
url: pac_url,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log::debug!(target: "app", "PAC配置未启用");
|
|
||||||
Ok(AsyncAutoproxy::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
|
||||||
// macOS: 使用 scutil --proxy 命令
|
|
||||||
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
return Ok(AsyncAutoproxy::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
log::debug!(target: "app", "scutil output: {stdout}");
|
|
||||||
|
|
||||||
let mut pac_enabled = false;
|
|
||||||
let mut pac_url = String::new();
|
|
||||||
|
|
||||||
// 解析 scutil 输出
|
|
||||||
for line in stdout.lines() {
|
|
||||||
let line = line.trim();
|
|
||||||
if line.contains("ProxyAutoConfigEnable") && line.contains("1") {
|
|
||||||
pac_enabled = true;
|
|
||||||
} else if line.contains("ProxyAutoConfigURLString") {
|
|
||||||
// 正确解析包含冒号的URL
|
|
||||||
// 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac"
|
|
||||||
if let Some(colon_pos) = line.find(" : ") {
|
|
||||||
pac_url = line[colon_pos + 3..].trim().to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
|
|
||||||
|
|
||||||
Ok(AsyncAutoproxy {
|
|
||||||
enable: pac_enabled && !pac_url.is_empty(),
|
|
||||||
url: pac_url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
|
||||||
// Linux: 检查环境变量和GNOME设置
|
|
||||||
|
|
||||||
// 首先检查环境变量
|
|
||||||
if let Ok(auto_proxy) = std::env::var("auto_proxy") {
|
|
||||||
if !auto_proxy.is_empty() {
|
|
||||||
return Ok(AsyncAutoproxy {
|
|
||||||
enable: true,
|
|
||||||
url: auto_proxy,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
|
||||||
let output = Command::new("gsettings")
|
|
||||||
.args(["get", "org.gnome.system.proxy", "mode"])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(output) = output {
|
|
||||||
if output.status.success() {
|
|
||||||
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
if mode.contains("auto") {
|
|
||||||
// 获取 PAC URL
|
|
||||||
let pac_output = Command::new("gsettings")
|
|
||||||
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(pac_output) = pac_output {
|
|
||||||
if pac_output.status.success() {
|
|
||||||
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
|
|
||||||
.trim()
|
|
||||||
.trim_matches('\'')
|
|
||||||
.trim_matches('"')
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if !pac_url.is_empty() {
|
|
||||||
return Ok(AsyncAutoproxy {
|
|
||||||
enable: true,
|
|
||||||
url: pac_url,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AsyncAutoproxy::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
|
||||||
// Windows: 使用注册表直接读取代理设置
|
|
||||||
tokio::task::spawn_blocking(move || -> Result<AsyncSysproxy> {
|
|
||||||
Self::get_system_proxy_from_registry()
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn get_system_proxy_from_registry() -> Result<AsyncSysproxy> {
|
|
||||||
use std::ptr;
|
|
||||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
|
||||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
|
||||||
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
|
||||||
.encode_utf16()
|
|
||||||
.collect::<Vec<u16>>();
|
|
||||||
|
|
||||||
let mut hkey: HKEY = ptr::null_mut();
|
|
||||||
let result =
|
|
||||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
|
||||||
|
|
||||||
if result != 0 {
|
|
||||||
return Ok(AsyncSysproxy::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查代理是否启用
|
|
||||||
let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::<Vec<u16>>();
|
|
||||||
let mut proxy_enable: DWORD = 0;
|
|
||||||
let mut buffer_size: DWORD = 4;
|
|
||||||
let mut value_type: DWORD = 0;
|
|
||||||
|
|
||||||
let enable_result = RegQueryValueExW(
|
|
||||||
hkey,
|
|
||||||
proxy_enable_name.as_ptr(),
|
|
||||||
ptr::null_mut(),
|
|
||||||
&mut value_type,
|
|
||||||
&mut proxy_enable as *mut DWORD as *mut u8,
|
|
||||||
&mut buffer_size,
|
|
||||||
);
|
|
||||||
|
|
||||||
if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 {
|
|
||||||
RegCloseKey(hkey);
|
|
||||||
return Ok(AsyncSysproxy::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取代理服务器设置
|
|
||||||
let proxy_server_name = "ProxyServer\0".encode_utf16().collect::<Vec<u16>>();
|
|
||||||
let mut buffer = vec![0u16; 1024];
|
|
||||||
let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD;
|
|
||||||
let mut value_type: DWORD = 0;
|
|
||||||
|
|
||||||
let server_result = RegQueryValueExW(
|
|
||||||
hkey,
|
|
||||||
proxy_server_name.as_ptr(),
|
|
||||||
ptr::null_mut(),
|
|
||||||
&mut value_type,
|
|
||||||
buffer.as_mut_ptr() as *mut u8,
|
|
||||||
&mut buffer_size,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut proxy_server = String::new();
|
|
||||||
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
|
||||||
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
|
|
||||||
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取代理绕过列表
|
|
||||||
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
|
|
||||||
let mut bypass_buffer = vec![0u16; 1024];
|
|
||||||
let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD;
|
|
||||||
let mut bypass_value_type: DWORD = 0;
|
|
||||||
|
|
||||||
let override_result = RegQueryValueExW(
|
|
||||||
hkey,
|
|
||||||
proxy_override_name.as_ptr(),
|
|
||||||
ptr::null_mut(),
|
|
||||||
&mut bypass_value_type,
|
|
||||||
bypass_buffer.as_mut_ptr() as *mut u8,
|
|
||||||
&mut bypass_buffer_size,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut bypass_list = String::new();
|
|
||||||
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
|
|
||||||
let end_pos = bypass_buffer
|
|
||||||
.iter()
|
|
||||||
.position(|&x| x == 0)
|
|
||||||
.unwrap_or(bypass_buffer.len());
|
|
||||||
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
|
|
||||||
}
|
|
||||||
|
|
||||||
RegCloseKey(hkey);
|
|
||||||
|
|
||||||
if !proxy_server.is_empty() {
|
|
||||||
// 解析服务器地址和端口
|
|
||||||
let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') {
|
|
||||||
let host = proxy_server[..colon_pos].to_string();
|
|
||||||
let port = proxy_server[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
|
||||||
(host, port)
|
|
||||||
} else {
|
|
||||||
(proxy_server, 8080)
|
|
||||||
};
|
|
||||||
|
|
||||||
log::debug!(target: "app", "从注册表读取到代理设置: {}:{}, bypass: {}", host, port, bypass_list);
|
|
||||||
|
|
||||||
Ok(AsyncSysproxy {
|
|
||||||
enable: true,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
bypass: bypass_list,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(AsyncSysproxy::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
|
||||||
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
return Ok(AsyncSysproxy::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
log::debug!(target: "app", "scutil proxy output: {stdout}");
|
|
||||||
|
|
||||||
let mut http_enabled = false;
|
|
||||||
let mut http_host = String::new();
|
|
||||||
let mut http_port = 8080u16;
|
|
||||||
let mut exceptions = Vec::new();
|
|
||||||
|
|
||||||
for line in stdout.lines() {
|
|
||||||
let line = line.trim();
|
|
||||||
if line.contains("HTTPEnable") && line.contains("1") {
|
|
||||||
http_enabled = true;
|
|
||||||
} else if line.contains("HTTPProxy") && !line.contains("Port") {
|
|
||||||
if let Some(host_part) = line.split(':').nth(1) {
|
|
||||||
http_host = host_part.trim().to_string();
|
|
||||||
}
|
|
||||||
} else if line.contains("HTTPPort") {
|
|
||||||
if let Some(port_part) = line.split(':').nth(1) {
|
|
||||||
if let Ok(port) = port_part.trim().parse::<u16>() {
|
|
||||||
http_port = port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if line.contains("ExceptionsList") {
|
|
||||||
// 解析异常列表
|
|
||||||
if let Some(list_part) = line.split(':').nth(1) {
|
|
||||||
let list = list_part.trim();
|
|
||||||
if !list.is_empty() {
|
|
||||||
exceptions.push(list.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AsyncSysproxy {
|
|
||||||
enable: http_enabled && !http_host.is_empty(),
|
|
||||||
host: http_host,
|
|
||||||
port: http_port,
|
|
||||||
bypass: exceptions.join(","),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
|
||||||
// Linux: 检查环境变量和桌面环境设置
|
|
||||||
|
|
||||||
// 首先检查环境变量
|
|
||||||
if let Ok(http_proxy) = std::env::var("http_proxy") {
|
|
||||||
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) {
|
|
||||||
return Ok(proxy_info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(https_proxy) = std::env::var("https_proxy") {
|
|
||||||
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) {
|
|
||||||
return Ok(proxy_info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
|
||||||
let mode_output = Command::new("gsettings")
|
|
||||||
.args(["get", "org.gnome.system.proxy", "mode"])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(mode_output) = mode_output {
|
|
||||||
if mode_output.status.success() {
|
|
||||||
let mode = String::from_utf8_lossy(&mode_output.stdout)
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
if mode.contains("manual") {
|
|
||||||
// 获取HTTP代理设置
|
|
||||||
let host_result = Command::new("gsettings")
|
|
||||||
.args(["get", "org.gnome.system.proxy.http", "host"])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let port_result = Command::new("gsettings")
|
|
||||||
.args(["get", "org.gnome.system.proxy.http", "port"])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) {
|
|
||||||
if host_output.status.success() && port_output.status.success() {
|
|
||||||
let host = String::from_utf8_lossy(&host_output.stdout)
|
|
||||||
.trim()
|
|
||||||
.trim_matches('\'')
|
|
||||||
.trim_matches('"')
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let port = String::from_utf8_lossy(&port_output.stdout)
|
|
||||||
.trim()
|
|
||||||
.parse::<u16>()
|
|
||||||
.unwrap_or(8080);
|
|
||||||
|
|
||||||
if !host.is_empty() {
|
|
||||||
return Ok(AsyncSysproxy {
|
|
||||||
enable: true,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
bypass: String::new(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AsyncSysproxy::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn parse_proxy_url(proxy_url: &str) -> Result<AsyncSysproxy> {
|
|
||||||
// 解析形如 "http://proxy.example.com:8080" 的URL
|
|
||||||
let url = proxy_url.trim();
|
|
||||||
|
|
||||||
// 移除协议前缀
|
|
||||||
let url = if let Some(stripped) = url.strip_prefix("http://") {
|
|
||||||
stripped
|
|
||||||
} else if let Some(stripped) = url.strip_prefix("https://") {
|
|
||||||
stripped
|
|
||||||
} else {
|
|
||||||
url
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解析主机和端口
|
|
||||||
let (host, port) = if let Some(colon_pos) = url.rfind(':') {
|
|
||||||
let host = url[..colon_pos].to_string();
|
|
||||||
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
|
||||||
(host, port)
|
|
||||||
} else {
|
|
||||||
(url.to_string(), 8080)
|
|
||||||
};
|
|
||||||
|
|
||||||
if host.is_empty() {
|
|
||||||
return Err(anyhow!("无效的代理URL"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AsyncSysproxy {
|
|
||||||
enable: true,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
bypass: std::env::var("no_proxy").unwrap_or_default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,19 @@
|
|||||||
use crate::{config::Config, utils::dirs};
|
use crate::config::Config;
|
||||||
|
use crate::utils::dirs;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
||||||
use std::{
|
use std::collections::HashMap;
|
||||||
collections::HashMap,
|
use std::env::{consts::OS, temp_dir};
|
||||||
env::{consts::OS, temp_dir},
|
use std::fs;
|
||||||
fs,
|
use std::io::Write;
|
||||||
io::Write,
|
use std::path::PathBuf;
|
||||||
path::PathBuf,
|
use std::sync::Arc;
|
||||||
sync::Arc,
|
use std::time::Duration;
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use zip::write::SimpleFileOptions;
|
use zip::write::SimpleFileOptions;
|
||||||
|
|
||||||
// 应用版本常量,来自 tauri.conf.json
|
|
||||||
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
const TIMEOUT_UPLOAD: u64 = 300; // 上传超时 5 分钟
|
const TIMEOUT_UPLOAD: u64 = 300; // 上传超时 5 分钟
|
||||||
const TIMEOUT_DOWNLOAD: u64 = 300; // 下载超时 5 分钟
|
const TIMEOUT_DOWNLOAD: u64 = 300; // 下载超时 5 分钟
|
||||||
const TIMEOUT_LIST: u64 = 3; // 列表超时 30 秒
|
const TIMEOUT_LIST: u64 = 3; // 列表超时 30 秒
|
||||||
@@ -108,15 +104,6 @@ impl WebDavClient {
|
|||||||
reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.timeout(Duration::from_secs(op.timeout()))
|
.timeout(Duration::from_secs(op.timeout()))
|
||||||
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
|
|
||||||
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
|
||||||
// 允许所有请求类型的重定向,包括PUT
|
|
||||||
if attempt.previous().len() >= 5 {
|
|
||||||
attempt.error("重定向次数过多")
|
|
||||||
} else {
|
|
||||||
attempt.follow()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -124,13 +111,12 @@ impl WebDavClient {
|
|||||||
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
|
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
// 尝试检查目录是否存在,如果不存在尝试创建,但创建失败不报错
|
// 确保备份目录存在
|
||||||
if client
|
let list_result = client
|
||||||
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
|
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
|
||||||
.await
|
.await;
|
||||||
.is_err()
|
if list_result.is_err() {
|
||||||
{
|
client.mkcol(dirs::BACKUP_DIR).await?;
|
||||||
let _ = client.mkcol(dirs::BACKUP_DIR).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存客户端
|
// 缓存客户端
|
||||||
@@ -150,41 +136,9 @@ impl WebDavClient {
|
|||||||
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
|
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(Operation::Upload).await?;
|
||||||
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
|
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??;
|
||||||
let file_content = fs::read(&file_path)?;
|
Ok(())
|
||||||
|
|
||||||
// 添加超时保护
|
|
||||||
let upload_result = timeout(
|
|
||||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
|
||||||
client.put(&webdav_path, file_content.clone()),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match upload_result {
|
|
||||||
Err(_) => {
|
|
||||||
log::warn!("Upload timed out, retrying once");
|
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
||||||
timeout(
|
|
||||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
|
||||||
client.put(&webdav_path, file_content),
|
|
||||||
)
|
|
||||||
.await??;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
log::warn!("Upload failed, retrying once: {e}");
|
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
||||||
timeout(
|
|
||||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
|
||||||
client.put(&webdav_path, file_content),
|
|
||||||
)
|
|
||||||
.await??;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Ok(Ok(_)) => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
|
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
|
||||||
@@ -234,7 +188,7 @@ impl WebDavClient {
|
|||||||
|
|
||||||
pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||||
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
|
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
|
||||||
let zip_file_name = format!("{OS}-backup-{now}.zip");
|
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
|
||||||
let zip_path = temp_dir().join(&zip_file_name);
|
let zip_path = temp_dir().join(&zip_file_name);
|
||||||
|
|
||||||
let file = fs::File::create(&zip_path)?;
|
let file = fs::File::create(&zip_path)?;
|
||||||
|
|||||||
@@ -1,571 +0,0 @@
|
|||||||
use parking_lot::RwLock;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::{mpsc, oneshot};
|
|
||||||
use tokio::time::{sleep, timeout, Duration};
|
|
||||||
|
|
||||||
use crate::config::{Config, IVerge};
|
|
||||||
use crate::core::async_proxy_query::AsyncProxyQuery;
|
|
||||||
use crate::logging_error;
|
|
||||||
use crate::utils::logging::Type;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use sysproxy::{Autoproxy, Sysproxy};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ProxyEvent {
|
|
||||||
/// 配置变更事件
|
|
||||||
ConfigChanged,
|
|
||||||
/// 强制检查代理状态
|
|
||||||
#[allow(dead_code)]
|
|
||||||
ForceCheck,
|
|
||||||
/// 启用系统代理
|
|
||||||
#[allow(dead_code)]
|
|
||||||
EnableProxy,
|
|
||||||
/// 禁用系统代理
|
|
||||||
#[allow(dead_code)]
|
|
||||||
DisableProxy,
|
|
||||||
/// 切换到PAC模式
|
|
||||||
#[allow(dead_code)]
|
|
||||||
SwitchToPac,
|
|
||||||
/// 切换到HTTP代理模式
|
|
||||||
#[allow(dead_code)]
|
|
||||||
SwitchToHttp,
|
|
||||||
/// 应用启动事件
|
|
||||||
AppStarted,
|
|
||||||
/// 应用关闭事件
|
|
||||||
#[allow(dead_code)]
|
|
||||||
AppStopping,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ProxyState {
|
|
||||||
pub sys_enabled: bool,
|
|
||||||
pub pac_enabled: bool,
|
|
||||||
pub auto_proxy: Autoproxy,
|
|
||||||
pub sys_proxy: Sysproxy,
|
|
||||||
pub last_updated: std::time::Instant,
|
|
||||||
pub is_healthy: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ProxyState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
sys_enabled: false,
|
|
||||||
pac_enabled: false,
|
|
||||||
auto_proxy: Autoproxy {
|
|
||||||
enable: false,
|
|
||||||
url: "".to_string(),
|
|
||||||
},
|
|
||||||
sys_proxy: Sysproxy {
|
|
||||||
enable: false,
|
|
||||||
host: "127.0.0.1".to_string(),
|
|
||||||
port: 7890,
|
|
||||||
bypass: "".to_string(),
|
|
||||||
},
|
|
||||||
last_updated: std::time::Instant::now(),
|
|
||||||
is_healthy: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EventDrivenProxyManager {
|
|
||||||
state: Arc<RwLock<ProxyState>>,
|
|
||||||
event_sender: mpsc::UnboundedSender<ProxyEvent>,
|
|
||||||
query_sender: mpsc::UnboundedSender<QueryRequest>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct QueryRequest {
|
|
||||||
response_tx: oneshot::Sender<Autoproxy>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置结构体移到外部
|
|
||||||
struct ProxyConfig {
|
|
||||||
sys_enabled: bool,
|
|
||||||
pac_enabled: bool,
|
|
||||||
guard_enabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
|
|
||||||
|
|
||||||
impl EventDrivenProxyManager {
|
|
||||||
pub fn global() -> &'static EventDrivenProxyManager {
|
|
||||||
&PROXY_MANAGER
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new() -> Self {
|
|
||||||
let state = Arc::new(RwLock::new(ProxyState::default()));
|
|
||||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
|
||||||
let (query_tx, query_rx) = mpsc::unbounded_channel();
|
|
||||||
|
|
||||||
Self::start_event_loop(state.clone(), event_rx, query_rx);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
event_sender: event_tx,
|
|
||||||
query_sender: query_tx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取自动代理配置(缓存)
|
|
||||||
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
|
|
||||||
self.state.read().auto_proxy.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 异步获取最新的自动代理配置
|
|
||||||
pub async fn get_auto_proxy_async(&self) -> Autoproxy {
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
let query = QueryRequest { response_tx: tx };
|
|
||||||
|
|
||||||
if self.query_sender.send(query).is_err() {
|
|
||||||
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
|
|
||||||
return self.get_auto_proxy_cached();
|
|
||||||
}
|
|
||||||
|
|
||||||
match timeout(Duration::from_secs(5), rx).await {
|
|
||||||
Ok(Ok(result)) => result,
|
|
||||||
_ => {
|
|
||||||
log::warn!(target: "app", "查询超时,返回缓存数据");
|
|
||||||
self.get_auto_proxy_cached()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 通知配置变更
|
|
||||||
pub fn notify_config_changed(&self) {
|
|
||||||
self.send_event(ProxyEvent::ConfigChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 通知应用启动
|
|
||||||
pub fn notify_app_started(&self) {
|
|
||||||
self.send_event(ProxyEvent::AppStarted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 通知应用即将关闭
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn notify_app_stopping(&self) {
|
|
||||||
self.send_event(ProxyEvent::AppStopping);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 启用系统代理
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn enable_proxy(&self) {
|
|
||||||
self.send_event(ProxyEvent::EnableProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 禁用系统代理
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn disable_proxy(&self) {
|
|
||||||
self.send_event(ProxyEvent::DisableProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 强制检查代理状态
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn force_check(&self) {
|
|
||||||
self.send_event(ProxyEvent::ForceCheck);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_event(&self, event: ProxyEvent) {
|
|
||||||
if let Err(e) = self.event_sender.send(event) {
|
|
||||||
log::error!(target: "app", "发送代理事件失败: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_event_loop(
|
|
||||||
state: Arc<RwLock<ProxyState>>,
|
|
||||||
mut event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
|
|
||||||
mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
|
||||||
) {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
log::info!(target: "app", "事件驱动代理管理器启动");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
event = event_rx.recv() => {
|
|
||||||
match event {
|
|
||||||
Some(event) => {
|
|
||||||
log::debug!(target: "app", "处理代理事件: {event:?}");
|
|
||||||
Self::handle_event(&state, event).await;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
log::info!(target: "app", "事件通道关闭,代理管理器停止");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
query = query_rx.recv() => {
|
|
||||||
match query {
|
|
||||||
Some(query) => {
|
|
||||||
let result = Self::handle_query(&state).await;
|
|
||||||
let _ = query.response_tx.send(result);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
log::info!(target: "app", "查询通道关闭");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
|
|
||||||
match event {
|
|
||||||
ProxyEvent::ConfigChanged | ProxyEvent::ForceCheck => {
|
|
||||||
Self::update_proxy_config(state).await;
|
|
||||||
}
|
|
||||||
ProxyEvent::EnableProxy => {
|
|
||||||
Self::enable_system_proxy(state).await;
|
|
||||||
}
|
|
||||||
ProxyEvent::DisableProxy => {
|
|
||||||
Self::disable_system_proxy(state).await;
|
|
||||||
}
|
|
||||||
ProxyEvent::SwitchToPac => {
|
|
||||||
Self::switch_proxy_mode(state, true).await;
|
|
||||||
}
|
|
||||||
ProxyEvent::SwitchToHttp => {
|
|
||||||
Self::switch_proxy_mode(state, false).await;
|
|
||||||
}
|
|
||||||
ProxyEvent::AppStarted => {
|
|
||||||
Self::initialize_proxy_state(state).await;
|
|
||||||
}
|
|
||||||
ProxyEvent::AppStopping => {
|
|
||||||
log::info!(target: "app", "清理代理状态");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_query(state: &Arc<RwLock<ProxyState>>) -> Autoproxy {
|
|
||||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
|
||||||
s.auto_proxy = auto_proxy.clone();
|
|
||||||
});
|
|
||||||
|
|
||||||
auto_proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
|
||||||
log::info!(target: "app", "初始化代理状态");
|
|
||||||
|
|
||||||
let config = Self::get_proxy_config();
|
|
||||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
|
||||||
let sys_proxy = Self::get_sys_proxy_with_timeout().await;
|
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
|
||||||
s.sys_enabled = config.sys_enabled;
|
|
||||||
s.pac_enabled = config.pac_enabled;
|
|
||||||
s.auto_proxy = auto_proxy;
|
|
||||||
s.sys_proxy = sys_proxy;
|
|
||||||
s.is_healthy = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
|
||||||
log::debug!(target: "app", "更新代理配置");
|
|
||||||
|
|
||||||
let config = Self::get_proxy_config();
|
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
|
||||||
s.sys_enabled = config.sys_enabled;
|
|
||||||
s.pac_enabled = config.pac_enabled;
|
|
||||||
});
|
|
||||||
|
|
||||||
if config.guard_enabled && config.sys_enabled {
|
|
||||||
Self::check_and_restore_proxy(state).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
|
|
||||||
let (sys_enabled, pac_enabled) = {
|
|
||||||
let s = state.read();
|
|
||||||
(s.sys_enabled, s.pac_enabled)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !sys_enabled {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!(target: "app", "检查代理状态");
|
|
||||||
|
|
||||||
if pac_enabled {
|
|
||||||
Self::check_and_restore_pac_proxy(state).await;
|
|
||||||
} else {
|
|
||||||
Self::check_and_restore_sys_proxy(state).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
|
|
||||||
let current = Self::get_auto_proxy_with_timeout().await;
|
|
||||||
let expected = Self::get_expected_pac_config();
|
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
|
||||||
s.auto_proxy = current.clone();
|
|
||||||
});
|
|
||||||
|
|
||||||
if !current.enable || current.url != expected.url {
|
|
||||||
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
|
|
||||||
Self::restore_pac_proxy(&expected.url).await;
|
|
||||||
|
|
||||||
sleep(Duration::from_millis(500)).await;
|
|
||||||
let restored = Self::get_auto_proxy_with_timeout().await;
|
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
|
||||||
s.is_healthy = restored.enable && restored.url == expected.url;
|
|
||||||
s.auto_proxy = restored;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
|
|
||||||
let current = Self::get_sys_proxy_with_timeout().await;
|
|
||||||
let expected = Self::get_expected_sys_proxy();
|
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
|
||||||
s.sys_proxy = current.clone();
|
|
||||||
});
|
|
||||||
|
|
||||||
if !current.enable || current.host != expected.host || current.port != expected.port {
|
|
||||||
log::info!(target: "app", "系统代理设置异常,正在恢复...");
|
|
||||||
Self::restore_sys_proxy(&expected).await;
|
|
||||||
|
|
||||||
sleep(Duration::from_millis(500)).await;
|
|
||||||
let restored = Self::get_sys_proxy_with_timeout().await;
|
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
|
||||||
s.is_healthy = restored.enable
|
|
||||||
&& restored.host == expected.host
|
|
||||||
&& restored.port == expected.port;
|
|
||||||
s.sys_proxy = restored;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
|
|
||||||
log::info!(target: "app", "启用系统代理");
|
|
||||||
|
|
||||||
let pac_enabled = state.read().pac_enabled;
|
|
||||||
|
|
||||||
if pac_enabled {
|
|
||||||
let expected = Self::get_expected_pac_config();
|
|
||||||
Self::restore_pac_proxy(&expected.url).await;
|
|
||||||
} else {
|
|
||||||
let expected = Self::get_expected_sys_proxy();
|
|
||||||
Self::restore_sys_proxy(&expected).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::check_and_restore_proxy(state).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
|
|
||||||
log::info!(target: "app", "禁用系统代理");
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
let disabled_sys = Sysproxy::default();
|
|
||||||
let disabled_auto = Autoproxy::default();
|
|
||||||
|
|
||||||
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
|
||||||
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
|
|
||||||
log::info!(target: "app", "切换到{}模式", if to_pac { "PAC" } else { "HTTP代理" });
|
|
||||||
|
|
||||||
if to_pac {
|
|
||||||
let disabled_sys = Sysproxy::default();
|
|
||||||
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
|
||||||
|
|
||||||
let expected = Self::get_expected_pac_config();
|
|
||||||
Self::restore_pac_proxy(&expected.url).await;
|
|
||||||
} else {
|
|
||||||
let disabled_auto = Autoproxy::default();
|
|
||||||
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
|
||||||
|
|
||||||
let expected = Self::get_expected_sys_proxy();
|
|
||||||
Self::restore_sys_proxy(&expected).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac);
|
|
||||||
Self::check_and_restore_proxy(state).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_auto_proxy_with_timeout() -> Autoproxy {
|
|
||||||
let async_proxy = AsyncProxyQuery::get_auto_proxy().await;
|
|
||||||
|
|
||||||
// 转换为兼容的结构
|
|
||||||
Autoproxy {
|
|
||||||
enable: async_proxy.enable,
|
|
||||||
url: async_proxy.url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_sys_proxy_with_timeout() -> Sysproxy {
|
|
||||||
let async_proxy = AsyncProxyQuery::get_system_proxy().await;
|
|
||||||
|
|
||||||
// 转换为兼容的结构
|
|
||||||
Sysproxy {
|
|
||||||
enable: async_proxy.enable,
|
|
||||||
host: async_proxy.host,
|
|
||||||
port: async_proxy.port,
|
|
||||||
bypass: async_proxy.bypass,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统一的状态更新方法
|
|
||||||
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut ProxyState),
|
|
||||||
{
|
|
||||||
let mut state_guard = state.write();
|
|
||||||
update_fn(&mut state_guard);
|
|
||||||
state_guard.last_updated = std::time::Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_proxy_config() -> ProxyConfig {
|
|
||||||
let verge_config = Config::verge();
|
|
||||||
let verge = verge_config.latest();
|
|
||||||
ProxyConfig {
|
|
||||||
sys_enabled: verge.enable_system_proxy.unwrap_or(false),
|
|
||||||
pac_enabled: verge.proxy_auto_config.unwrap_or(false),
|
|
||||||
guard_enabled: verge.enable_proxy_guard.unwrap_or(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_expected_pac_config() -> Autoproxy {
|
|
||||||
let verge_config = Config::verge();
|
|
||||||
let verge = verge_config.latest();
|
|
||||||
let (proxy_host, pac_port) = (
|
|
||||||
verge
|
|
||||||
.proxy_host
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "127.0.0.1".to_string()),
|
|
||||||
IVerge::get_singleton_port(),
|
|
||||||
);
|
|
||||||
Autoproxy {
|
|
||||||
enable: true,
|
|
||||||
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_expected_sys_proxy() -> Sysproxy {
|
|
||||||
let verge_config = Config::verge();
|
|
||||||
let verge = verge_config.latest();
|
|
||||||
let port = verge
|
|
||||||
.verge_mixed_port
|
|
||||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
|
||||||
let proxy_host = verge
|
|
||||||
.proxy_host
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "127.0.0.1".to_string());
|
|
||||||
|
|
||||||
Sysproxy {
|
|
||||||
enable: true,
|
|
||||||
host: proxy_host,
|
|
||||||
port,
|
|
||||||
bypass: Self::get_bypass_config(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bypass_config() -> String {
|
|
||||||
let verge_config = Config::verge();
|
|
||||||
let verge = verge_config.latest();
|
|
||||||
let use_default = verge.use_default_bypass.unwrap_or(true);
|
|
||||||
let custom_bypass = verge.system_proxy_bypass.clone().unwrap_or_default();
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let default_bypass = "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")]
|
|
||||||
let default_bypass =
|
|
||||||
"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";
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let default_bypass = "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>";
|
|
||||||
|
|
||||||
if custom_bypass.is_empty() {
|
|
||||||
default_bypass.to_string()
|
|
||||||
} else if use_default {
|
|
||||||
format!("{default_bypass},{custom_bypass}")
|
|
||||||
} else {
|
|
||||||
custom_bypass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restore_pac_proxy(expected_url: &str) {
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
let new_autoproxy = Autoproxy {
|
|
||||||
enable: true,
|
|
||||||
url: expected_url.to_string(),
|
|
||||||
};
|
|
||||||
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
Self::execute_sysproxy_command(&["pac", expected_url]).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restore_sys_proxy(expected: &Sysproxy) {
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
logging_error!(Type::System, true, expected.set_system_proxy());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let address = format!("{}:{}", expected.host, expected.port);
|
|
||||||
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
async fn execute_sysproxy_command(args: &[&str]) {
|
|
||||||
use crate::utils::dirs;
|
|
||||||
#[allow(unused_imports)] // creation_flags必须
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
use tokio::process::Command;
|
|
||||||
|
|
||||||
let binary_path = match dirs::service_path() {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "获取服务路径失败: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
|
||||||
if !sysproxy_exe.exists() {
|
|
||||||
log::error!(target: "app", "sysproxy.exe 不存在");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = Command::new(sysproxy_exe)
|
|
||||||
.args(args)
|
|
||||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match output {
|
|
||||||
Ok(output) => {
|
|
||||||
if !output.status.success() {
|
|
||||||
log::error!(target: "app", "执行sysproxy命令失败: {:?}", args);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
log::error!(target: "app", "sysproxy错误输出: {}", stderr);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::debug!(target: "app", "成功执行sysproxy命令: {:?}", args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "执行sysproxy命令出错: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,293 +1,31 @@
|
|||||||
|
use crate::log_err;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::{
|
use std::sync::Arc;
|
||||||
sync::{
|
|
||||||
atomic::{AtomicU64, Ordering},
|
|
||||||
mpsc, Arc,
|
|
||||||
},
|
|
||||||
thread,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||||
|
use tauri_plugin_shell::process::CommandChild;
|
||||||
|
|
||||||
use crate::{logging, utils::logging::Type};
|
#[derive(Debug, Default, Clone)]
|
||||||
|
|
||||||
/// 不同类型的前端通知
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
enum FrontendEvent {
|
|
||||||
RefreshClash,
|
|
||||||
RefreshVerge,
|
|
||||||
NoticeMessage { status: String, message: String },
|
|
||||||
ProfileChanged { current_profile_id: String },
|
|
||||||
TimerUpdated { profile_index: String },
|
|
||||||
StartupCompleted,
|
|
||||||
ProfileUpdateStarted { uid: String },
|
|
||||||
ProfileUpdateCompleted { uid: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 事件发送统计和监控
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct EventStats {
|
|
||||||
total_sent: AtomicU64,
|
|
||||||
total_errors: AtomicU64,
|
|
||||||
last_error_time: RwLock<Option<Instant>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 存储启动期间的错误消息
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct ErrorMessage {
|
|
||||||
status: String,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 全局前端通知系统
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct NotificationSystem {
|
|
||||||
sender: Option<mpsc::Sender<FrontendEvent>>,
|
|
||||||
worker_handle: Option<thread::JoinHandle<()>>,
|
|
||||||
is_running: bool,
|
|
||||||
stats: EventStats,
|
|
||||||
last_emit_time: RwLock<Instant>,
|
|
||||||
/// 当通知系统失败超过阈值时,进入紧急模式
|
|
||||||
emergency_mode: RwLock<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NotificationSystem {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NotificationSystem {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
sender: None,
|
|
||||||
worker_handle: None,
|
|
||||||
is_running: false,
|
|
||||||
stats: EventStats::default(),
|
|
||||||
last_emit_time: RwLock::new(Instant::now()),
|
|
||||||
emergency_mode: RwLock::new(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 启动通知处理线程
|
|
||||||
fn start(&mut self) {
|
|
||||||
if self.is_running {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
self.sender = Some(tx);
|
|
||||||
self.is_running = true;
|
|
||||||
|
|
||||||
*self.last_emit_time.write() = Instant::now();
|
|
||||||
|
|
||||||
self.worker_handle = Some(
|
|
||||||
thread::Builder::new()
|
|
||||||
.name("frontend-notifier".into())
|
|
||||||
.spawn(move || {
|
|
||||||
let handle = Handle::global();
|
|
||||||
|
|
||||||
while !handle.is_exiting() {
|
|
||||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
|
||||||
Ok(event) => {
|
|
||||||
let system_guard = handle.notification_system.read();
|
|
||||||
if system_guard.as_ref().is_none() {
|
|
||||||
log::warn!("NotificationSystem not found in handle while processing event.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let system = system_guard.as_ref().unwrap();
|
|
||||||
|
|
||||||
let is_emergency = *system.emergency_mode.read();
|
|
||||||
|
|
||||||
if is_emergency {
|
|
||||||
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
|
||||||
if status == "info" {
|
|
||||||
log::warn!(
|
|
||||||
"Emergency mode active, skipping info message"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(window) = handle.get_window() {
|
|
||||||
*system.last_emit_time.write() = Instant::now();
|
|
||||||
|
|
||||||
let (event_name_str, payload_result) = match event {
|
|
||||||
FrontendEvent::RefreshClash => {
|
|
||||||
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
|
|
||||||
}
|
|
||||||
FrontendEvent::RefreshVerge => {
|
|
||||||
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
|
|
||||||
}
|
|
||||||
FrontendEvent::NoticeMessage { status, message } => {
|
|
||||||
match serde_json::to_value((status, message)) {
|
|
||||||
Ok(p) => ("verge://notice-message", Ok(p)),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to serialize NoticeMessage payload: {e}");
|
|
||||||
("verge://notice-message", Err(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FrontendEvent::ProfileChanged { current_profile_id } => {
|
|
||||||
("profile-changed", Ok(serde_json::json!(current_profile_id)))
|
|
||||||
}
|
|
||||||
FrontendEvent::TimerUpdated { profile_index } => {
|
|
||||||
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
|
|
||||||
}
|
|
||||||
FrontendEvent::StartupCompleted => {
|
|
||||||
("verge://startup-completed", Ok(serde_json::json!(null)))
|
|
||||||
}
|
|
||||||
FrontendEvent::ProfileUpdateStarted { uid } => {
|
|
||||||
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
|
|
||||||
}
|
|
||||||
FrontendEvent::ProfileUpdateCompleted { uid } => {
|
|
||||||
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(payload) = payload_result {
|
|
||||||
match window.emit(event_name_str, payload) {
|
|
||||||
Ok(_) => {
|
|
||||||
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
|
||||||
// 记录成功发送的事件
|
|
||||||
if log::log_enabled!(log::Level::Debug) {
|
|
||||||
log::debug!("Successfully emitted event: {event_name_str}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to emit event {event_name_str}: {e}");
|
|
||||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
|
||||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
|
||||||
|
|
||||||
let errors = system.stats.total_errors.load(Ordering::SeqCst);
|
|
||||||
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
|
||||||
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
|
||||||
log::warn!(
|
|
||||||
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
|
|
||||||
);
|
|
||||||
*system.emergency_mode.write() = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
|
||||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
|
||||||
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::warn!("No window found, skipping event emit.");
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(20));
|
|
||||||
}
|
|
||||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
|
||||||
log::info!(
|
|
||||||
"Notification channel disconnected, exiting worker thread"
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Notification worker thread exiting");
|
|
||||||
})
|
|
||||||
.expect("Failed to start notification worker thread"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 发送事件到队列
|
|
||||||
fn send_event(&self, event: FrontendEvent) -> bool {
|
|
||||||
if *self.emergency_mode.read() {
|
|
||||||
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
|
||||||
if status == "info" {
|
|
||||||
log::info!("Skipping info message in emergency mode");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(sender) = &self.sender {
|
|
||||||
match sender.send(event) {
|
|
||||||
Ok(_) => true,
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to send event to notification queue: {e:?}");
|
|
||||||
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
|
||||||
*self.stats.last_error_time.write() = Some(Instant::now());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::warn!("Notification system not started, can't send event");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shutdown(&mut self) {
|
|
||||||
log::info!("NotificationSystem shutdown initiated");
|
|
||||||
self.is_running = false;
|
|
||||||
|
|
||||||
// 先关闭发送端,让接收端知道不会再有新消息
|
|
||||||
if let Some(sender) = self.sender.take() {
|
|
||||||
drop(sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置超时避免无限等待
|
|
||||||
if let Some(handle) = self.worker_handle.take() {
|
|
||||||
match handle.join() {
|
|
||||||
Ok(_) => {
|
|
||||||
log::info!("NotificationSystem worker thread joined successfully");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("NotificationSystem worker thread join failed: {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("NotificationSystem shutdown completed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||||
pub is_exiting: Arc<RwLock<bool>>,
|
pub is_exiting: Arc<RwLock<bool>>,
|
||||||
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
pub core_process: Arc<RwLock<Option<CommandChild>>>,
|
||||||
startup_completed: Arc<RwLock<bool>>,
|
|
||||||
notification_system: Arc<RwLock<Option<NotificationSystem>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Handle {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
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)),
|
|
||||||
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
pub fn global() -> &'static Handle {
|
pub fn global() -> &'static Handle {
|
||||||
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
||||||
HANDLE.get_or_init(Handle::default)
|
|
||||||
|
HANDLE.get_or_init(|| Handle {
|
||||||
|
app_handle: Arc::new(RwLock::new(None)),
|
||||||
|
is_exiting: Arc::new(RwLock::new(false)),
|
||||||
|
core_process: Arc::new(RwLock::new(None)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self, app_handle: &AppHandle) {
|
pub fn init(&self, app_handle: &AppHandle) {
|
||||||
{
|
let mut handle = self.app_handle.write();
|
||||||
let mut handle = self.app_handle.write();
|
*handle = Some(app_handle.clone());
|
||||||
*handle = Some(app_handle.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut system_opt = self.notification_system.write();
|
|
||||||
if let Some(system) = system_opt.as_mut() {
|
|
||||||
system.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app_handle(&self) -> Option<AppHandle> {
|
pub fn app_handle(&self) -> Option<AppHandle> {
|
||||||
@@ -295,7 +33,7 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_window(&self) -> Option<WebviewWindow> {
|
pub fn get_window(&self) -> Option<WebviewWindow> {
|
||||||
let app_handle = self.app_handle()?;
|
let app_handle = self.app_handle().unwrap();
|
||||||
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
|
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
|
||||||
if window.is_none() {
|
if window.is_none() {
|
||||||
log::debug!(target:"app", "main window not found");
|
log::debug!(target:"app", "main window not found");
|
||||||
@@ -304,213 +42,48 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_clash() {
|
pub fn refresh_clash() {
|
||||||
let handle = Self::global();
|
if let Some(window) = Self::global().get_window() {
|
||||||
if handle.is_exiting() {
|
log_err!(window.emit("verge://refresh-clash-config", "yes"));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
system.send_event(FrontendEvent::RefreshClash);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_verge() {
|
pub fn refresh_verge() {
|
||||||
let handle = Self::global();
|
if let Some(window) = Self::global().get_window() {
|
||||||
if handle.is_exiting() {
|
log_err!(window.emit("verge://refresh-verge-config", "yes"));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
system.send_event(FrontendEvent::RefreshVerge);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notify_profile_changed(profile_id: String) {
|
#[allow(unused)]
|
||||||
let handle = Self::global();
|
pub fn refresh_profiles() {
|
||||||
if handle.is_exiting() {
|
if let Some(window) = Self::global().get_window() {
|
||||||
return;
|
log_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
system.send_event(FrontendEvent::ProfileChanged {
|
|
||||||
current_profile_id: profile_id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
log::warn!(
|
|
||||||
"Notification system not initialized when trying to send ProfileChanged event."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notify_timer_updated(profile_index: String) {
|
|
||||||
let handle = Self::global();
|
|
||||||
if handle.is_exiting() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
system.send_event(FrontendEvent::TimerUpdated { profile_index });
|
|
||||||
} else {
|
|
||||||
log::warn!(
|
|
||||||
"Notification system not initialized when trying to send TimerUpdated event."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notify_startup_completed() {
|
|
||||||
let handle = Self::global();
|
|
||||||
if handle.is_exiting() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
system.send_event(FrontendEvent::StartupCompleted);
|
|
||||||
} else {
|
|
||||||
log::warn!(
|
|
||||||
"Notification system not initialized when trying to send StartupCompleted event."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notify_profile_update_started(uid: String) {
|
|
||||||
let handle = Self::global();
|
|
||||||
if handle.is_exiting() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
system.send_event(FrontendEvent::ProfileUpdateStarted { uid });
|
|
||||||
} else {
|
|
||||||
log::warn!("Notification system not initialized when trying to send ProfileUpdateStarted event.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notify_profile_update_completed(uid: String) {
|
|
||||||
let handle = Self::global();
|
|
||||||
if handle.is_exiting() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
system.send_event(FrontendEvent::ProfileUpdateCompleted { uid });
|
|
||||||
} else {
|
|
||||||
log::warn!("Notification system not initialized when trying to send ProfileUpdateCompleted event.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 通知前端显示消息队列
|
|
||||||
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||||
let handle = Self::global();
|
if let Some(window) = Self::global().get_window() {
|
||||||
let status_str = status.into();
|
log_err!(window.emit("verge://notice-message", (status.into(), msg.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 handle.is_exiting() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
system.send_event(FrontendEvent::NoticeMessage {
|
|
||||||
status: status_str,
|
|
||||||
message: msg_str,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
);
|
|
||||||
|
|
||||||
// 启动单独线程处理启动错误,避免阻塞主线程
|
|
||||||
let thread_result = thread::Builder::new()
|
|
||||||
.name("startup-errors-sender".into())
|
|
||||||
.spawn(move || {
|
|
||||||
thread::sleep(Duration::from_secs(2));
|
|
||||||
|
|
||||||
let handle = Handle::global();
|
|
||||||
if handle.is_exiting() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
for error in errors {
|
|
||||||
if handle.is_exiting() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
system.send_event(FrontendEvent::NoticeMessage {
|
|
||||||
status: error.status,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
thread::sleep(Duration::from_millis(300));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(e) = thread_result {
|
|
||||||
log::error!("Failed to spawn startup errors thread: {e}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_is_exiting(&self) {
|
pub fn set_is_exiting(&self) {
|
||||||
let mut is_exiting = self.is_exiting.write();
|
let mut is_exiting = self.is_exiting.write();
|
||||||
*is_exiting = true;
|
*is_exiting = true;
|
||||||
|
}
|
||||||
|
|
||||||
let mut system_opt = self.notification_system.write();
|
pub fn set_core_process(&self, process: CommandChild) {
|
||||||
if let Some(system) = system_opt.as_mut() {
|
let mut core_process = self.core_process.write();
|
||||||
system.shutdown();
|
*core_process = Some(process);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn take_core_process(&self) -> Option<CommandChild> {
|
||||||
|
let mut core_process = self.core_process.write();
|
||||||
|
core_process.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否有运行中的核心进程
|
||||||
|
pub fn has_core_process(&self) -> bool {
|
||||||
|
self.core_process.read().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_exiting(&self) -> bool {
|
pub fn is_exiting(&self) -> bool {
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
use crate::utils::notification::{notify_event, NotificationEvent};
|
use crate::core::handle;
|
||||||
use crate::{
|
use crate::{config::Config, feat, log_err};
|
||||||
config::Config, core::handle, feat, logging, logging_error,
|
use crate::utils::resolve;
|
||||||
module::lightweight::entry_lightweight_mode, utils::logging::Type,
|
|
||||||
};
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||||
|
use tauri::async_runtime;
|
||||||
|
|
||||||
pub struct Hotkey {
|
pub struct Hotkey {
|
||||||
current: Arc<Mutex<Vec<String>>>,
|
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hotkey {
|
impl Hotkey {
|
||||||
@@ -27,26 +26,19 @@ impl Hotkey {
|
|||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
||||||
|
|
||||||
logging!(
|
println!("Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
||||||
debug,
|
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"Initializing global hotkeys: {}",
|
|
||||||
enable_global_hotkey
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// 如果全局热键被禁用,则不注册热键
|
||||||
if !enable_global_hotkey {
|
if !enable_global_hotkey {
|
||||||
|
println!("Global hotkey is disabled, skipping registration");
|
||||||
|
log::info!(target: "app", "Global hotkey is disabled, skipping registration");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||||
logging!(
|
println!("Found {} hotkeys to register", hotkeys.len());
|
||||||
debug,
|
log::info!(target: "app", "Found {} hotkeys to register", hotkeys.len());
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"Has {} hotkeys need to register",
|
|
||||||
hotkeys.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
for hotkey in hotkeys.iter() {
|
for hotkey in hotkeys.iter() {
|
||||||
let mut iter = hotkey.split(',');
|
let mut iter = hotkey.split(',');
|
||||||
@@ -55,51 +47,28 @@ impl Hotkey {
|
|||||||
|
|
||||||
match (key, func) {
|
match (key, func) {
|
||||||
(Some(key), Some(func)) => {
|
(Some(key), Some(func)) => {
|
||||||
logging!(
|
println!("Registering hotkey: {} -> {}", key, func);
|
||||||
debug,
|
log::info!(target: "app", "Registering hotkey: {} -> {}", key, func);
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"Registering hotkey: {} -> {}",
|
|
||||||
key,
|
|
||||||
func
|
|
||||||
);
|
|
||||||
if let Err(e) = self.register(key, func) {
|
if let Err(e) = self.register(key, func) {
|
||||||
logging!(
|
println!("Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
||||||
error,
|
log::error!(target: "app", "Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"Failed to register hotkey {} -> {}: {:?}",
|
|
||||||
key,
|
|
||||||
func,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
logging!(
|
println!("Successfully registered hotkey {} -> {}", key, func);
|
||||||
debug,
|
log::info!(target: "app", "Successfully registered hotkey {} -> {}", key, func);
|
||||||
Type::Hotkey,
|
|
||||||
"Successfully registered hotkey {} -> {}",
|
|
||||||
key,
|
|
||||||
func
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let key = key.unwrap_or("None");
|
let key = key.unwrap_or("None");
|
||||||
let func = func.unwrap_or("None");
|
let func = func.unwrap_or("None");
|
||||||
logging!(
|
println!("Invalid hotkey configuration: `{key}`:`{func}`");
|
||||||
error,
|
log::error!(target: "app", "Invalid hotkey configuration: `{key}`:`{func}`");
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"Invalid hotkey configuration: `{}`:`{}`",
|
|
||||||
key,
|
|
||||||
func
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.current.lock().clone_from(hotkeys);
|
self.current.lock().clone_from(hotkeys);
|
||||||
} else {
|
} else {
|
||||||
logging!(debug, Type::Hotkey, "No hotkeys configured");
|
println!("No hotkeys configured");
|
||||||
|
log::info!(target: "app", "No hotkeys configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -116,121 +85,67 @@ impl Hotkey {
|
|||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
|
|
||||||
logging!(
|
println!("Attempting to register hotkey: {} for function: {}", hotkey, func);
|
||||||
debug,
|
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
|
||||||
Type::Hotkey,
|
|
||||||
"Attempting to register hotkey: {} for function: {}",
|
|
||||||
hotkey,
|
|
||||||
func
|
|
||||||
);
|
|
||||||
|
|
||||||
if manager.is_registered(hotkey) {
|
if manager.is_registered(hotkey) {
|
||||||
logging!(
|
println!("Hotkey {} was already registered, unregistering first", hotkey);
|
||||||
debug,
|
log::info!(target: "app", "Hotkey {} was already registered, unregistering first", hotkey);
|
||||||
Type::Hotkey,
|
|
||||||
"Hotkey {} was already registered, unregistering first",
|
|
||||||
hotkey
|
|
||||||
);
|
|
||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_handle_clone = app_handle.clone();
|
let f = match func.trim() {
|
||||||
let f: Box<dyn Fn() + Send + Sync> = match func.trim() {
|
|
||||||
"open_or_close_dashboard" => {
|
"open_or_close_dashboard" => {
|
||||||
let app_handle = app_handle_clone.clone();
|
println!("Registering open_or_close_dashboard function");
|
||||||
Box::new(move || {
|
log::info!(target: "app", "Registering open_or_close_dashboard function");
|
||||||
logging!(
|
|| {
|
||||||
debug,
|
println!("=== Hotkey Dashboard Window Operation Start ===");
|
||||||
Type::Hotkey,
|
log::info!(target: "app", "=== Hotkey Dashboard Window Operation Start ===");
|
||||||
true,
|
|
||||||
"=== Hotkey Dashboard Window Operation Start ==="
|
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||||
);
|
async_runtime::spawn_blocking(|| {
|
||||||
|
println!("Toggle dashboard window visibility");
|
||||||
|
log::info!(target: "app", "Toggle dashboard window visibility");
|
||||||
|
|
||||||
|
// 检查窗口是否存在
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
// 如果窗口可见,则隐藏它
|
||||||
|
if window.is_visible().unwrap_or(false) {
|
||||||
|
println!("Window is visible, hiding it");
|
||||||
|
log::info!(target: "app", "Window is visible, hiding it");
|
||||||
|
let _ = window.hide();
|
||||||
|
} else {
|
||||||
|
// 如果窗口不可见,则显示它
|
||||||
|
println!("Window is hidden, showing it");
|
||||||
|
log::info!(target: "app", "Window is hidden, showing it");
|
||||||
|
if window.is_minimized().unwrap_or(false) {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
}
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果窗口不存在,创建一个新窗口
|
||||||
|
println!("Window does not exist, creating a new one");
|
||||||
|
log::info!(target: "app", "Window does not exist, creating a new one");
|
||||||
|
resolve::create_window();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("=== Hotkey Dashboard Window Operation End ===");
|
||||||
|
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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),
|
||||||
|
"quit" => || feat::quit(Some(0)),
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"Using unified WindowManager for hotkey operation (bypass debounce)"
|
|
||||||
);
|
|
||||||
|
|
||||||
crate::feat::open_or_close_dashboard_hotkey();
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Hotkey,
|
|
||||||
"=== Hotkey Dashboard Window Operation End ==="
|
|
||||||
);
|
|
||||||
notify_event(&app_handle, NotificationEvent::DashboardToggled);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"clash_mode_rule" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::change_clash_mode("rule".into());
|
|
||||||
notify_event(
|
|
||||||
&app_handle,
|
|
||||||
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"clash_mode_global" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::change_clash_mode("global".into());
|
|
||||||
notify_event(
|
|
||||||
&app_handle,
|
|
||||||
NotificationEvent::ClashModeChanged { mode: "Global" },
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"clash_mode_direct" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::change_clash_mode("direct".into());
|
|
||||||
notify_event(
|
|
||||||
&app_handle,
|
|
||||||
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"toggle_system_proxy" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::toggle_system_proxy();
|
|
||||||
notify_event(&app_handle, NotificationEvent::SystemProxyToggled);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"toggle_tun_mode" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::toggle_tun_mode(None);
|
|
||||||
notify_event(&app_handle, NotificationEvent::TunModeToggled);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"entry_lightweight_mode" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
entry_lightweight_mode();
|
|
||||||
notify_event(&app_handle, NotificationEvent::LightweightModeEntered);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"quit" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::quit();
|
|
||||||
notify_event(&app_handle, NotificationEvent::AppQuit);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"hide" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::hide();
|
|
||||||
notify_event(&app_handle, NotificationEvent::AppHidden);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
println!("Invalid function: {}", func);
|
||||||
|
log::error!(target: "app", "Invalid function: {}", func);
|
||||||
bail!("invalid function \"{func}\"");
|
bail!("invalid function \"{func}\"");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -239,30 +154,34 @@ impl Hotkey {
|
|||||||
|
|
||||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
||||||
if event.state == ShortcutState::Pressed {
|
if event.state == ShortcutState::Pressed {
|
||||||
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey);
|
println!("Hotkey pressed: {:?}", hotkey);
|
||||||
|
log::info!(target: "app", "Hotkey pressed: {:?}", hotkey);
|
||||||
|
|
||||||
if hotkey.key == Code::KeyQ && is_quit {
|
if hotkey.key == Code::KeyQ && is_quit {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
if window.is_focused().unwrap_or(false) {
|
if window.is_focused().unwrap_or(false) {
|
||||||
logging!(debug, Type::Hotkey, "Executing quit function");
|
println!("Executing quit function");
|
||||||
|
log::info!(target: "app", "Executing quit function");
|
||||||
f();
|
f();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
// 直接执行函数,不做任何状态检查
|
||||||
|
println!("Executing function directly");
|
||||||
let is_enable_global_hotkey = Config::verge()
|
log::info!(target: "app", "Executing function directly");
|
||||||
.latest()
|
|
||||||
.enable_global_hotkey
|
// 获取轻量模式状态和全局热键状态
|
||||||
.unwrap_or(true);
|
let is_lite_mode = Config::verge().latest().enable_lite_mode.unwrap_or(false);
|
||||||
|
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||||
if is_enable_global_hotkey {
|
|
||||||
|
// 在轻量模式下或配置了全局热键时,始终执行热键功能
|
||||||
|
if is_lite_mode || is_enable_global_hotkey {
|
||||||
f();
|
f();
|
||||||
} else {
|
} else if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
use crate::utils::window_manager::WindowManager;
|
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||||
let is_visible = WindowManager::is_main_window_visible();
|
let is_visible = window.is_visible().unwrap_or(false);
|
||||||
let is_focused = WindowManager::is_main_window_focused();
|
let is_focused = window.is_focused().unwrap_or(false);
|
||||||
|
|
||||||
if is_focused && is_visible {
|
if is_focused && is_visible {
|
||||||
f();
|
f();
|
||||||
}
|
}
|
||||||
@@ -271,13 +190,8 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
logging!(
|
println!("Successfully registered hotkey {} for {}", hotkey, func);
|
||||||
debug,
|
log::info!(target: "app", "Successfully registered hotkey {} for {}", hotkey, func);
|
||||||
Type::Hotkey,
|
|
||||||
"Successfully registered hotkey {} for {}",
|
|
||||||
hotkey,
|
|
||||||
func
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +199,7 @@ impl Hotkey {
|
|||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey);
|
log::debug!(target: "app", "unregister hotkey {hotkey}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +215,7 @@ impl Hotkey {
|
|||||||
});
|
});
|
||||||
|
|
||||||
add.iter().for_each(|(key, func)| {
|
add.iter().for_each(|(key, func)| {
|
||||||
logging_error!(Type::Hotkey, self.register(key, func));
|
log_err!(self.register(key, func));
|
||||||
});
|
});
|
||||||
|
|
||||||
*current = new_hotkeys;
|
*current = new_hotkeys;
|
||||||
@@ -316,9 +230,9 @@ impl Hotkey {
|
|||||||
let func = iter.next();
|
let func = iter.next();
|
||||||
let key = iter.next();
|
let key = iter.next();
|
||||||
|
|
||||||
if let (Some(func), Some(key)) = (func, key) {
|
if func.is_some() && key.is_some() {
|
||||||
let func = func.trim();
|
let func = func.unwrap().trim();
|
||||||
let key = key.trim();
|
let key = key.unwrap().trim();
|
||||||
map.insert(key, func);
|
map.insert(key, func);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -358,13 +272,7 @@ impl Drop for Hotkey {
|
|||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||||
logging!(
|
log::error!(target:"app", "Error unregistering all hotkeys: {:?}", e);
|
||||||
error,
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"Error unregistering all hotkeys: {:?}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
pub mod async_proxy_query;
|
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
mod core;
|
mod core;
|
||||||
pub mod event_driven_proxy;
|
|
||||||
pub mod handle;
|
pub mod handle;
|
||||||
pub mod hotkey;
|
pub mod hotkey;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod service_ipc;
|
|
||||||
pub mod sysopt;
|
pub mod sysopt;
|
||||||
pub mod timer;
|
pub mod timer;
|
||||||
pub mod tray;
|
pub mod tray;
|
||||||
pub mod win_uwp;
|
pub mod win_uwp;
|
||||||
|
|
||||||
pub use self::{core::*, event_driven_proxy::EventDrivenProxyManager, timer::Timer};
|
pub use self::core::*;
|
||||||
|
|||||||
@@ -1,340 +0,0 @@
|
|||||||
use crate::{logging, utils::logging::Type};
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use hmac::{Hmac, Mac};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
|
|
||||||
r"\\.\pipe\clash-verge-service"
|
|
||||||
} else {
|
|
||||||
"/tmp/clash-verge-service.sock"
|
|
||||||
};
|
|
||||||
|
|
||||||
// 定义命令类型
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum IpcCommand {
|
|
||||||
GetClash,
|
|
||||||
GetVersion,
|
|
||||||
StartClash,
|
|
||||||
StopClash,
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPC消息格式
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct IpcRequest {
|
|
||||||
pub id: String,
|
|
||||||
pub timestamp: u64,
|
|
||||||
pub command: IpcCommand,
|
|
||||||
pub payload: serde_json::Value,
|
|
||||||
pub signature: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct IpcResponse {
|
|
||||||
pub id: String,
|
|
||||||
pub success: bool,
|
|
||||||
pub data: Option<serde_json::Value>,
|
|
||||||
pub error: Option<String>,
|
|
||||||
pub signature: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 密钥派生函数
|
|
||||||
fn derive_secret_key() -> Vec<u8> {
|
|
||||||
// to do
|
|
||||||
// 从系统安全存储中获取或从程序安装时生成的密钥文件中读取
|
|
||||||
let unique_app_id = "clash-verge-app-secret-fuck-me-until-daylight";
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(unique_app_id.as_bytes());
|
|
||||||
hasher.finalize().to_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建带签名的请求
|
|
||||||
pub fn create_signed_request(
|
|
||||||
command: IpcCommand,
|
|
||||||
payload: serde_json::Value,
|
|
||||||
) -> Result<IpcRequest> {
|
|
||||||
let id = nanoid::nanoid!(32);
|
|
||||||
let timestamp = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
let unsigned_request = IpcRequest {
|
|
||||||
id: id.clone(),
|
|
||||||
timestamp,
|
|
||||||
command: command.clone(),
|
|
||||||
payload: payload.clone(),
|
|
||||||
signature: String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let unsigned_json = serde_json::to_string(&unsigned_request)?;
|
|
||||||
let signature = sign_message(&unsigned_json)?;
|
|
||||||
|
|
||||||
Ok(IpcRequest {
|
|
||||||
id,
|
|
||||||
timestamp,
|
|
||||||
command,
|
|
||||||
payload,
|
|
||||||
signature,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 签名消息
|
|
||||||
fn sign_message(message: &str) -> Result<String> {
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
|
||||||
|
|
||||||
let secret_key = derive_secret_key();
|
|
||||||
let mut mac = HmacSha256::new_from_slice(&secret_key).context("HMAC初始化失败")?;
|
|
||||||
|
|
||||||
mac.update(message.as_bytes());
|
|
||||||
let result = mac.finalize();
|
|
||||||
let signature = hex::encode(result.into_bytes());
|
|
||||||
|
|
||||||
Ok(signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证响应签名
|
|
||||||
pub fn verify_response_signature(response: &IpcResponse) -> Result<bool> {
|
|
||||||
let verification_response = IpcResponse {
|
|
||||||
id: response.id.clone(),
|
|
||||||
success: response.success,
|
|
||||||
data: response.data.clone(),
|
|
||||||
error: response.error.clone(),
|
|
||||||
signature: String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = serde_json::to_string(&verification_response)?;
|
|
||||||
let expected_signature = sign_message(&message)?;
|
|
||||||
|
|
||||||
Ok(expected_signature == response.signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPC连接管理-win
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub async fn send_ipc_request(
|
|
||||||
command: IpcCommand,
|
|
||||||
payload: serde_json::Value,
|
|
||||||
) -> Result<IpcResponse> {
|
|
||||||
use std::{
|
|
||||||
ffi::CString,
|
|
||||||
fs::File,
|
|
||||||
io::{Read, Write},
|
|
||||||
os::windows::io::{FromRawHandle, RawHandle},
|
|
||||||
ptr,
|
|
||||||
};
|
|
||||||
use winapi::um::{
|
|
||||||
fileapi::{CreateFileA, OPEN_EXISTING},
|
|
||||||
handleapi::INVALID_HANDLE_VALUE,
|
|
||||||
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
|
|
||||||
};
|
|
||||||
|
|
||||||
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
|
|
||||||
|
|
||||||
let command_type = format!("{:?}", command);
|
|
||||||
|
|
||||||
let request = match create_signed_request(command, payload) {
|
|
||||||
Ok(req) => req,
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let request_json = serde_json::to_string(&request)?;
|
|
||||||
|
|
||||||
let result = tokio::task::spawn_blocking(move || -> Result<IpcResponse> {
|
|
||||||
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
|
|
||||||
Ok(name) => name,
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "创建CString失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("创建CString失败: {}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let handle = unsafe {
|
|
||||||
CreateFileA(
|
|
||||||
c_pipe_name.as_ptr(),
|
|
||||||
GENERIC_READ | GENERIC_WRITE,
|
|
||||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
||||||
ptr::null_mut(),
|
|
||||||
OPEN_EXISTING,
|
|
||||||
0,
|
|
||||||
ptr::null_mut(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if handle == INVALID_HANDLE_VALUE {
|
|
||||||
let error = std::io::Error::last_os_error();
|
|
||||||
logging!(
|
|
||||||
error,
|
|
||||||
Type::Service,
|
|
||||||
true,
|
|
||||||
"连接到服务命名管道失败: {}",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) };
|
|
||||||
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
|
|
||||||
|
|
||||||
let request_bytes = request_json.as_bytes();
|
|
||||||
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
|
||||||
|
|
||||||
if let Err(e) = pipe.write_all(&len_bytes) {
|
|
||||||
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = pipe.write_all(request_bytes) {
|
|
||||||
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = pipe.flush() {
|
|
||||||
logging!(error, Type::Service, true, "刷新管道失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("刷新管道失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut response_len_bytes = [0u8; 4];
|
|
||||||
if let Err(e) = pipe.read_exact(&mut response_len_bytes) {
|
|
||||||
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
|
||||||
|
|
||||||
let mut response_bytes = vec![0u8; response_len];
|
|
||||||
if let Err(e) = pipe.read_exact(&mut response_bytes) {
|
|
||||||
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "服务响应解析失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match verify_response_signature(&response) {
|
|
||||||
Ok(valid) => {
|
|
||||||
if !valid {
|
|
||||||
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
|
||||||
bail!("服务响应签名验证失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Service,
|
|
||||||
true,
|
|
||||||
"IPC请求完成: 命令={}, 成功={}",
|
|
||||||
command_type,
|
|
||||||
response.success
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPC连接管理-unix
|
|
||||||
#[cfg(target_family = "unix")]
|
|
||||||
pub async fn send_ipc_request(
|
|
||||||
command: IpcCommand,
|
|
||||||
payload: serde_json::Value,
|
|
||||||
) -> Result<IpcResponse> {
|
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
|
|
||||||
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
|
|
||||||
|
|
||||||
let command_type = format!("{command:?}");
|
|
||||||
|
|
||||||
let request = match create_signed_request(command, payload) {
|
|
||||||
Ok(req) => req,
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let request_json = serde_json::to_string(&request)?;
|
|
||||||
|
|
||||||
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) {
|
|
||||||
Ok(s) => {
|
|
||||||
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
|
|
||||||
s
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let request_bytes = request_json.as_bytes();
|
|
||||||
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
|
||||||
|
|
||||||
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) {
|
|
||||||
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
|
|
||||||
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut response_len_bytes = [0u8; 4];
|
|
||||||
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) {
|
|
||||||
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
|
||||||
|
|
||||||
let mut response_bytes = vec![0u8; response_len];
|
|
||||||
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) {
|
|
||||||
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,);
|
|
||||||
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match verify_response_signature(&response) {
|
|
||||||
Ok(valid) => {
|
|
||||||
if !valid {
|
|
||||||
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
|
||||||
bail!("服务响应签名验证失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Service,
|
|
||||||
true,
|
|
||||||
"IPC请求完成: 命令={}, 成功={}",
|
|
||||||
command_type,
|
|
||||||
response.success
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,24 @@
|
|||||||
#[cfg(target_os = "windows")]
|
use crate::core::handle::Handle;
|
||||||
use crate::utils::autostart as startup_shortcut;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::{handle::Handle, EventDrivenProxyManager},
|
log_err,
|
||||||
logging, logging_error,
|
|
||||||
utils::logging::Type,
|
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
use sysproxy::{Autoproxy, Sysproxy};
|
use sysproxy::{Autoproxy, Sysproxy};
|
||||||
use tauri::async_runtime::Mutex as TokioMutex;
|
use tauri::async_runtime::Mutex as TokioMutex;
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
pub struct Sysopt {
|
pub struct Sysopt {
|
||||||
update_sysproxy: Arc<TokioMutex<bool>>,
|
update_sysproxy: Arc<TokioMutex<bool>>,
|
||||||
reset_sysproxy: Arc<TokioMutex<bool>>,
|
reset_sysproxy: Arc<TokioMutex<bool>>,
|
||||||
|
/// helps to auto launch the app
|
||||||
|
auto_launch: Arc<Mutex<bool>>,
|
||||||
|
/// record whether the guard async is running or not
|
||||||
|
guard_state: Arc<Mutex<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -43,7 +45,7 @@ fn get_bypass() -> String {
|
|||||||
if custom_bypass.is_empty() {
|
if custom_bypass.is_empty() {
|
||||||
DEFAULT_BYPASS.to_string()
|
DEFAULT_BYPASS.to_string()
|
||||||
} else if use_default {
|
} else if use_default {
|
||||||
format!("{DEFAULT_BYPASS},{custom_bypass}")
|
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
|
||||||
} else {
|
} else {
|
||||||
custom_bypass
|
custom_bypass
|
||||||
}
|
}
|
||||||
@@ -55,15 +57,13 @@ impl Sysopt {
|
|||||||
SYSOPT.get_or_init(|| Sysopt {
|
SYSOPT.get_or_init(|| Sysopt {
|
||||||
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||||
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||||
|
auto_launch: Arc::new(Mutex::new(false)),
|
||||||
|
guard_state: Arc::new(false.into()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
||||||
// 使用事件驱动代理管理器
|
self.guard_proxy();
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_app_started();
|
|
||||||
|
|
||||||
log::info!(target: "app", "已启用事件驱动代理守卫");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,16 +77,12 @@ impl Sysopt {
|
|||||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
let pac_port = IVerge::get_singleton_port();
|
let pac_port = IVerge::get_singleton_port();
|
||||||
|
|
||||||
let (sys_enable, pac_enable, proxy_host) = {
|
let (sys_enable, pac_enable) = {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge = verge.latest();
|
let verge = verge.latest();
|
||||||
(
|
(
|
||||||
verge.enable_system_proxy.unwrap_or(false),
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
verge.proxy_auto_config.unwrap_or(false),
|
verge.proxy_auto_config.unwrap_or(false),
|
||||||
verge
|
|
||||||
.proxy_host
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| String::from("127.0.0.1")),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,20 +90,18 @@ impl Sysopt {
|
|||||||
{
|
{
|
||||||
let mut sys = Sysproxy {
|
let mut sys = Sysproxy {
|
||||||
enable: false,
|
enable: false,
|
||||||
host: proxy_host.clone(),
|
host: String::from("127.0.0.1"),
|
||||||
port,
|
port,
|
||||||
bypass: get_bypass(),
|
bypass: get_bypass(),
|
||||||
};
|
};
|
||||||
let mut auto = Autoproxy {
|
let mut auto = Autoproxy {
|
||||||
enable: false,
|
enable: false,
|
||||||
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !sys_enable {
|
if !sys_enable {
|
||||||
sys.set_system_proxy()?;
|
sys.set_system_proxy()?;
|
||||||
auto.set_auto_proxy()?;
|
auto.set_auto_proxy()?;
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,8 +110,6 @@ impl Sysopt {
|
|||||||
auto.enable = true;
|
auto.enable = true;
|
||||||
sys.set_system_proxy()?;
|
sys.set_system_proxy()?;
|
||||||
auto.set_auto_proxy()?;
|
auto.set_auto_proxy()?;
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,20 +118,16 @@ impl Sysopt {
|
|||||||
sys.enable = true;
|
sys.enable = true;
|
||||||
auto.set_auto_proxy()?;
|
auto.set_auto_proxy()?;
|
||||||
sys.set_system_proxy()?;
|
sys.set_system_proxy()?;
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
if !sys_enable {
|
if !sys_enable {
|
||||||
let result = self.reset_sysproxy().await;
|
return self.reset_sysproxy().await;
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
use crate::{core::handle::Handle, utils::dirs};
|
use crate::core::handle::Handle;
|
||||||
|
use crate::utils::dirs;
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
@@ -153,7 +141,7 @@ impl Sysopt {
|
|||||||
|
|
||||||
let shell = app_handle.shell();
|
let shell = app_handle.shell();
|
||||||
let output = if pac_enable {
|
let output = if pac_enable {
|
||||||
let address = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
let address = format!("http://{}:{}/commands/pac", "127.0.0.1", pac_port);
|
||||||
let output = shell
|
let output = shell
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||||
.args(["pac", address.as_str()])
|
.args(["pac", address.as_str()])
|
||||||
@@ -162,7 +150,7 @@ impl Sysopt {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
output
|
output
|
||||||
} else {
|
} else {
|
||||||
let address = format!("{}:{}", proxy_host, port);
|
let address = format!("{}:{}", "127.0.0.1", port);
|
||||||
let bypass = get_bypass();
|
let bypass = get_bypass();
|
||||||
let output = shell
|
let output = shell
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||||
@@ -177,8 +165,6 @@ impl Sysopt {
|
|||||||
bail!("sysproxy exe run failed");
|
bail!("sysproxy exe run failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -190,16 +176,7 @@ impl Sysopt {
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
|
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
|
||||||
let mut autoproxy = match Autoproxy::get_auto_proxy() {
|
let mut autoproxy = Autoproxy::get_auto_proxy()?;
|
||||||
Ok(ap) => ap,
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
|
|
||||||
Autoproxy {
|
|
||||||
enable: false,
|
|
||||||
url: "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sysproxy.enable = false;
|
sysproxy.enable = false;
|
||||||
autoproxy.enable = false;
|
autoproxy.enable = false;
|
||||||
autoproxy.set_auto_proxy()?;
|
autoproxy.set_auto_proxy()?;
|
||||||
@@ -208,7 +185,8 @@ impl Sysopt {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
use crate::{core::handle::Handle, utils::dirs};
|
use crate::core::handle::Handle;
|
||||||
|
use crate::utils::dirs;
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
@@ -239,79 +217,132 @@ impl Sysopt {
|
|||||||
|
|
||||||
/// update the startup
|
/// update the startup
|
||||||
pub fn update_launch(&self) -> Result<()> {
|
pub fn update_launch(&self) -> Result<()> {
|
||||||
let enable_auto_launch = { Config::verge().latest().enable_auto_launch };
|
let _lock = self.auto_launch.lock();
|
||||||
let is_enable = enable_auto_launch.unwrap_or(false);
|
let enable = { Config::verge().latest().enable_auto_launch };
|
||||||
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
|
let enable = enable.unwrap_or(false);
|
||||||
|
let app_handle = Handle::global().app_handle().unwrap();
|
||||||
// 首先尝试使用快捷方式方法
|
let autostart_manager = app_handle.autolaunch();
|
||||||
#[cfg(target_os = "windows")]
|
println!("enable: {}", enable);
|
||||||
{
|
match enable {
|
||||||
if is_enable {
|
true => log_err!(autostart_manager.enable()),
|
||||||
if let Err(e) = startup_shortcut::create_shortcut() {
|
false => log_err!(autostart_manager.disable()),
|
||||||
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
|
};
|
||||||
// 如果快捷方式创建失败,回退到原来的方法
|
|
||||||
self.try_original_autostart_method(is_enable);
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
} else if let Err(e) = startup_shortcut::remove_shortcut() {
|
|
||||||
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
|
|
||||||
self.try_original_autostart_method(is_enable);
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
// 非Windows平台使用原来的方法
|
|
||||||
self.try_original_autostart_method(is_enable);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 尝试使用原来的自启动方法
|
fn guard_proxy(&self) {
|
||||||
fn try_original_autostart_method(&self, is_enable: bool) {
|
let _lock = self.guard_state.lock();
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
|
||||||
let autostart_manager = app_handle.autolaunch();
|
|
||||||
|
|
||||||
if is_enable {
|
tauri::async_runtime::spawn(async move {
|
||||||
logging_error!(Type::System, true, "{:?}", autostart_manager.enable());
|
// default duration is 10s
|
||||||
} else {
|
let mut wait_secs = 10u64;
|
||||||
logging_error!(Type::System, true, "{:?}", autostart_manager.disable());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取当前自启动的实际状态
|
loop {
|
||||||
pub fn get_launch_status(&self) -> Result<bool> {
|
sleep(Duration::from_secs(wait_secs)).await;
|
||||||
// 首先尝试检查快捷方式是否存在
|
|
||||||
#[cfg(target_os = "windows")]
|
let (enable, guard, guard_duration, pac) = {
|
||||||
{
|
let verge = Config::verge();
|
||||||
match startup_shortcut::is_shortcut_enabled() {
|
let verge = verge.latest();
|
||||||
Ok(enabled) => {
|
(
|
||||||
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
return Ok(enabled);
|
verge.enable_proxy_guard.unwrap_or(false),
|
||||||
|
verge.proxy_guard_duration.unwrap_or(10),
|
||||||
|
verge.proxy_auto_config.unwrap_or(false),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// stop loop
|
||||||
|
if !enable || !guard {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
|
// update duration
|
||||||
|
wait_secs = guard_duration;
|
||||||
|
|
||||||
|
log::debug!(target: "app", "try to guard the system proxy");
|
||||||
|
|
||||||
|
let sysproxy = Sysproxy::get_system_proxy();
|
||||||
|
let autoproxy = Autoproxy::get_auto_proxy();
|
||||||
|
if sysproxy.is_err() || autoproxy.is_err() {
|
||||||
|
log::error!(target: "app", "failed to get the system proxy");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回退到原来的方法
|
let sysproxy_enable = sysproxy.ok().map(|s| s.enable).unwrap_or(false);
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
let autoproxy_enable = autoproxy.ok().map(|s| s.enable).unwrap_or(false);
|
||||||
let autostart_manager = app_handle.autolaunch();
|
|
||||||
|
|
||||||
match autostart_manager.is_enabled() {
|
if sysproxy_enable || autoproxy_enable {
|
||||||
Ok(status) => {
|
continue;
|
||||||
log::info!(target: "app", "Auto launch status: {status}");
|
}
|
||||||
Ok(status)
|
|
||||||
|
let port = {
|
||||||
|
Config::verge()
|
||||||
|
.latest()
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port())
|
||||||
|
};
|
||||||
|
let pac_port = IVerge::get_singleton_port();
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
if pac {
|
||||||
|
let autoproxy = Autoproxy {
|
||||||
|
enable: true,
|
||||||
|
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
|
||||||
|
};
|
||||||
|
log_err!(autoproxy.set_auto_proxy());
|
||||||
|
} else {
|
||||||
|
let sysproxy = Sysproxy {
|
||||||
|
enable: true,
|
||||||
|
host: "127.0.0.1".into(),
|
||||||
|
port,
|
||||||
|
bypass: get_bypass(),
|
||||||
|
};
|
||||||
|
|
||||||
|
log_err!(sysproxy.set_system_proxy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use crate::core::handle::Handle;
|
||||||
|
use crate::utils::dirs;
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
|
let app_handle = Handle::global().app_handle().unwrap();
|
||||||
|
|
||||||
|
let binary_path = dirs::service_path().unwrap();
|
||||||
|
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||||
|
if !sysproxy_exe.exists() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shell = app_handle.shell();
|
||||||
|
let output = if pac {
|
||||||
|
let address = format!("http://{}:{}/commands/pac", "127.0.0.1", pac_port);
|
||||||
|
|
||||||
|
shell
|
||||||
|
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||||
|
.args(["pac", address.as_str()])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
} else {
|
||||||
|
let address = format!("{}:{}", "127.0.0.1", port);
|
||||||
|
let bypass = get_bypass();
|
||||||
|
|
||||||
|
shell
|
||||||
|
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||||
|
.args(["global", address.as_str(), bypass.as_ref()])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
if !output.status.success() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Err(e) => {
|
});
|
||||||
log::error!(target: "app", "Failed to get auto launch status: {e}");
|
|
||||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,24 @@
|
|||||||
use crate::{config::Config, feat, logging, logging_error, utils::logging::Type};
|
use crate::config::Config;
|
||||||
|
use crate::feat;
|
||||||
|
use crate::core::CoreManager;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::Mutex;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
type TaskID = u64;
|
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 {
|
pub struct Timer {
|
||||||
/// cron manager
|
/// cron manager
|
||||||
pub delay_timer: Arc<RwLock<DelayTimer>>,
|
delay_timer: Arc<Mutex<DelayTimer>>,
|
||||||
|
|
||||||
/// save the current state - using RwLock for better read concurrency
|
/// save the current state
|
||||||
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
|
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
|
||||||
|
|
||||||
/// increment id - kept as mutex since it's just a counter
|
/// increment id
|
||||||
pub timer_count: Arc<Mutex<TaskID>>,
|
timer_count: Arc<Mutex<TaskID>>,
|
||||||
|
|
||||||
/// Flag to mark if timer is initialized - atomic for better performance
|
|
||||||
pub initialized: Arc<std::sync::atomic::AtomicBool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timer {
|
impl Timer {
|
||||||
@@ -34,190 +26,68 @@ impl Timer {
|
|||||||
static TIMER: OnceCell<Timer> = OnceCell::new();
|
static TIMER: OnceCell<Timer> = OnceCell::new();
|
||||||
|
|
||||||
TIMER.get_or_init(|| Timer {
|
TIMER.get_or_init(|| Timer {
|
||||||
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
|
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
|
||||||
timer_map: Arc::new(RwLock::new(HashMap::new())),
|
timer_map: Arc::new(Mutex::new(HashMap::new())),
|
||||||
timer_count: Arc::new(Mutex::new(1)),
|
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<()> {
|
pub fn init(&self) -> Result<()> {
|
||||||
// Use compare_exchange for thread-safe initialization check
|
self.refresh()?;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let timer_map = self.timer_map.read();
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Timer,
|
|
||||||
"已注册的定时任务数量: {}",
|
|
||||||
timer_map.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
for (uid, task) in timer_map.iter() {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Timer,
|
|
||||||
"注册了定时任务 - uid={}, interval={}min, task_id={}",
|
|
||||||
uid,
|
|
||||||
task.interval_minutes,
|
|
||||||
task.task_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cur_timestamp = chrono::Local::now().timestamp();
|
let cur_timestamp = chrono::Local::now().timestamp();
|
||||||
|
|
||||||
// Collect profiles that need immediate update
|
let timer_map = self.timer_map.lock();
|
||||||
let profiles_to_update = if let Some(items) = Config::profiles().latest().get_items() {
|
let delay_timer = self.delay_timer.lock();
|
||||||
|
|
||||||
|
if let Some(items) = Config::profiles().latest().get_items() {
|
||||||
items
|
items
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| {
|
.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 updated = item.updated? as i64;
|
||||||
let uid = item.uid.as_ref()?;
|
|
||||||
|
|
||||||
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
if interval > 0 && cur_timestamp - updated >= interval {
|
||||||
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
|
Some(item)
|
||||||
Some(uid.clone())
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.for_each(|item| {
|
||||||
} else {
|
if let Some(uid) = item.uid.as_ref() {
|
||||||
Vec::new()
|
if let Some((task_id, _)) = timer_map.get(uid) {
|
||||||
};
|
crate::log_err!(delay_timer.advance_task(*task_id));
|
||||||
|
}
|
||||||
// Advance tasks outside of locks to minimize lock contention
|
|
||||||
if !profiles_to_update.is_empty() {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Timer,
|
|
||||||
"需要立即更新的配置数量: {}",
|
|
||||||
profiles_to_update.len()
|
|
||||||
);
|
|
||||||
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, "立即执行任务: uid={}", uid);
|
|
||||||
if let Err(e) = delay_timer.advance_task(task.task_id) {
|
|
||||||
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logging!(info, Type::Timer, "Timer initialization completed");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh timer tasks with better error handling
|
/// Correctly update all cron tasks
|
||||||
pub fn refresh(&self) -> Result<()> {
|
pub fn refresh(&self) -> Result<()> {
|
||||||
// Generate diff outside of lock to minimize lock contention
|
|
||||||
let diff_map = self.gen_diff();
|
let diff_map = self.gen_diff();
|
||||||
|
|
||||||
if diff_map.is_empty() {
|
let mut timer_map = self.timer_map.lock();
|
||||||
logging!(debug, Type::Timer, "No timer changes needed");
|
let mut delay_timer = self.delay_timer.lock();
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(
|
for (uid, diff) in diff_map.into_iter() {
|
||||||
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 {
|
|
||||||
match diff {
|
match diff {
|
||||||
DiffFlag::Del(tid) => {
|
DiffFlag::Del(tid) => {
|
||||||
timer_map.remove(&uid);
|
let _ = timer_map.remove(&uid);
|
||||||
if let Err(e) = delay_timer.remove_task(tid) {
|
crate::log_err!(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
DiffFlag::Add(tid, interval) => {
|
DiffFlag::Add(tid, val) => {
|
||||||
let task = TimerTask {
|
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||||
task_id: tid,
|
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||||
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::Mod(tid, interval) => {
|
DiffFlag::Mod(tid, val) => {
|
||||||
// Remove old task first
|
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||||
if let Err(e) = delay_timer.remove_task(tid) {
|
crate::log_err!(delay_timer.remove_task(tid));
|
||||||
logging!(
|
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,106 +95,59 @@ impl Timer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate map of profile UIDs to update intervals
|
/// generate a uid -> update_interval map
|
||||||
fn gen_map(&self) -> HashMap<String, u64> {
|
fn gen_map(&self) -> HashMap<String, u64> {
|
||||||
let mut new_map = HashMap::new();
|
let mut new_map = HashMap::new();
|
||||||
|
|
||||||
if let Some(items) = Config::profiles().latest().get_items() {
|
if let Some(items) = Config::profiles().latest().get_items() {
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if let Some(option) = item.option.as_ref() {
|
if item.option.is_some() {
|
||||||
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
|
let option = item.option.as_ref().unwrap();
|
||||||
if interval > 0 {
|
let interval = option.update_interval.unwrap_or(0);
|
||||||
logging!(
|
|
||||||
debug,
|
if interval > 0 {
|
||||||
Type::Timer,
|
new_map.insert(item.uid.clone().unwrap(), interval);
|
||||||
"找到定时更新配置: uid={}, interval={}min",
|
|
||||||
uid,
|
|
||||||
interval
|
|
||||||
);
|
|
||||||
new_map.insert(uid.clone(), interval);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Timer,
|
|
||||||
"生成的定时更新配置数量: {}",
|
|
||||||
new_map.len()
|
|
||||||
);
|
|
||||||
new_map
|
new_map
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate differences between current and new timer configuration
|
/// generate the diff map for refresh
|
||||||
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
||||||
let mut diff_map = HashMap::new();
|
let mut diff_map = HashMap::new();
|
||||||
|
|
||||||
|
let timer_map = self.timer_map.lock();
|
||||||
|
|
||||||
let new_map = self.gen_map();
|
let new_map = self.gen_map();
|
||||||
|
let cur_map = &timer_map;
|
||||||
|
|
||||||
// Read lock for comparing current state
|
cur_map.iter().for_each(|(uid, (tid, val))| {
|
||||||
let timer_map = self.timer_map.read();
|
let new_val = new_map.get(uid).unwrap_or(&0);
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Timer,
|
|
||||||
"当前 timer_map 大小: {}",
|
|
||||||
timer_map.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find tasks to modify or delete
|
if *new_val == 0 {
|
||||||
for (uid, task) in timer_map.iter() {
|
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
|
||||||
match new_map.get(uid) {
|
} else if new_val != val {
|
||||||
Some(&interval) if interval != task.interval_minutes => {
|
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
|
||||||
// Task exists but interval changed
|
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Timer,
|
|
||||||
"定时任务间隔变更: uid={}, 旧={}, 新={}",
|
|
||||||
uid,
|
|
||||||
task.interval_minutes,
|
|
||||||
interval
|
|
||||||
);
|
|
||||||
diff_map.insert(uid.clone(), DiffFlag::Mod(task.task_id, interval));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// Task no longer needed
|
|
||||||
logging!(debug, Type::Timer, "定时任务已删除: uid={}", uid);
|
|
||||||
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Task exists with same interval, no change needed
|
|
||||||
logging!(debug, Type::Timer, "定时任务保持不变: uid={}", uid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Find new tasks to add
|
let mut count = self.timer_count.lock();
|
||||||
let mut next_id = *self.timer_count.lock();
|
|
||||||
|
|
||||||
for (uid, &interval) in new_map.iter() {
|
new_map.iter().for_each(|(uid, val)| {
|
||||||
if !timer_map.contains_key(uid) {
|
if cur_map.get(uid).is_none() {
|
||||||
logging!(
|
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
|
||||||
debug,
|
|
||||||
Type::Timer,
|
*count += 1;
|
||||||
"新增定时任务: uid={}, interval={}min",
|
|
||||||
uid,
|
|
||||||
interval
|
|
||||||
);
|
|
||||||
diff_map.insert(uid.clone(), DiffFlag::Add(next_id, interval));
|
|
||||||
next_id += 1;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Update counter only if we added new tasks
|
|
||||||
if next_id > *self.timer_count.lock() {
|
|
||||||
*self.timer_count.lock() = next_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
|
|
||||||
diff_map
|
diff_map
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a timer task with better error handling
|
/// add a cron task
|
||||||
fn add_task(
|
fn add_task(
|
||||||
&self,
|
&self,
|
||||||
delay_timer: &mut DelayTimer,
|
delay_timer: &mut DelayTimer,
|
||||||
@@ -332,26 +155,12 @@ impl Timer {
|
|||||||
tid: TaskID,
|
tid: TaskID,
|
||||||
minutes: u64,
|
minutes: u64,
|
||||||
) -> Result<()> {
|
) -> 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()
|
let task = TaskBuilder::default()
|
||||||
.set_task_id(tid)
|
.set_task_id(tid)
|
||||||
.set_maximum_parallel_runnable_num(1)
|
.set_maximum_parallel_runnable_num(1)
|
||||||
.set_frequency_repeated_by_minutes(minutes)
|
.set_frequency_repeated_by_minutes(minutes)
|
||||||
.spawn_async_routine(move || {
|
// .set_frequency_repeated_by_seconds(minutes) // for test
|
||||||
let uid = uid.clone();
|
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
|
||||||
async move {
|
|
||||||
Self::async_task(uid).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.context("failed to create timer task")?;
|
.context("failed to create timer task")?;
|
||||||
|
|
||||||
delay_timer
|
delay_timer
|
||||||
@@ -361,118 +170,20 @@ impl Timer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get next update time for a profile
|
/// the task runner
|
||||||
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> {
|
|
||||||
logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid);
|
|
||||||
|
|
||||||
let timer_map = self.timer_map.read();
|
|
||||||
let task = match timer_map.get(uid) {
|
|
||||||
Some(t) => t,
|
|
||||||
None => {
|
|
||||||
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the profile updated timestamp
|
|
||||||
let profiles_config = Config::profiles();
|
|
||||||
let profiles = profiles_config.latest();
|
|
||||||
let items = match profiles.get_items() {
|
|
||||||
Some(i) => i,
|
|
||||||
None => {
|
|
||||||
logging!(warn, Type::Timer, "获取配置列表失败");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) {
|
|
||||||
Some(p) => p,
|
|
||||||
None => {
|
|
||||||
logging!(warn, Type::Timer, "找不到对应的配置,uid={}", uid);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let updated = profile.updated.unwrap_or(0) as i64;
|
|
||||||
|
|
||||||
// Calculate next update time
|
|
||||||
if updated > 0 && task.interval_minutes > 0 {
|
|
||||||
let next_time = updated + (task.interval_minutes as i64 * 60);
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Timer,
|
|
||||||
"计算得到下次更新时间: {}, uid={}",
|
|
||||||
next_time,
|
|
||||||
uid
|
|
||||||
);
|
|
||||||
Some(next_time)
|
|
||||||
} else {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Timer,
|
|
||||||
"更新时间或间隔无效,updated={}, interval={}",
|
|
||||||
updated,
|
|
||||||
task.interval_minutes
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emit update events for frontend notification
|
|
||||||
fn emit_update_event(_uid: &str, _is_start: bool) {
|
|
||||||
#[cfg(any(feature = "verge-dev", feature = "default"))]
|
|
||||||
{
|
|
||||||
if _is_start {
|
|
||||||
super::handle::Handle::notify_profile_update_started(_uid.to_string());
|
|
||||||
} else {
|
|
||||||
super::handle::Handle::notify_profile_update_completed(_uid.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Async task with better error handling and logging
|
|
||||||
async fn async_task(uid: String) {
|
async fn async_task(uid: String) {
|
||||||
let task_start = std::time::Instant::now();
|
log::info!(target: "app", "running timer task `{uid}`");
|
||||||
logging!(info, Type::Timer, "Running timer task for profile: {}", uid);
|
|
||||||
|
// 使用更轻量级的更新方式
|
||||||
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
|
if let Err(e) = feat::update_profile(uid.clone(), None).await {
|
||||||
Self::emit_update_event(&uid, true);
|
log::error!(target: "app", "timer task update error: {}", e);
|
||||||
|
return;
|
||||||
let is_current = Config::profiles().latest().current.as_ref() == Some(&uid);
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Timer,
|
|
||||||
"配置 {} 是否为当前激活配置: {}",
|
|
||||||
uid,
|
|
||||||
is_current
|
|
||||||
);
|
|
||||||
|
|
||||||
feat::update_profile(uid.clone(), None, Some(is_current)).await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(result) => match result {
|
|
||||||
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 update profile uid {}: {}", uid, e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
logging_error!(Type::Timer, false, "Timer task timed out for uid: {}", uid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit completed event
|
// 只有更新成功后才刷新配置
|
||||||
Self::emit_update_event(&uid, false);
|
if let Err(e) = CoreManager::global().update_config().await {
|
||||||
|
log::error!(target: "app", "timer task refresh error: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,171 +1,46 @@
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use tauri::tray::TrayIconBuilder;
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub mod speed_rate;
|
pub mod speed_rate;
|
||||||
|
use crate::module::mihomo::Rate;
|
||||||
use crate::{
|
use crate::{
|
||||||
cmd,
|
cmd,
|
||||||
config::Config,
|
config::Config,
|
||||||
feat, logging,
|
feat, resolve,
|
||||||
module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
|
utils::resolve::VERSION,
|
||||||
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
|
utils::{dirs, i18n::t},
|
||||||
Type,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use futures::StreamExt;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
#[cfg(target_os = "macos")]
|
||||||
fs,
|
use parking_lot::RwLock;
|
||||||
sync::atomic::{AtomicBool, Ordering},
|
#[cfg(target_os = "macos")]
|
||||||
time::{Duration, Instant},
|
pub use speed_rate::{SpeedRate, Traffic};
|
||||||
};
|
#[cfg(target_os = "macos")]
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauri::menu::{CheckMenuItem, IsMenuItem};
|
||||||
|
use tauri::AppHandle;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||||
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
tray::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId},
|
||||||
AppHandle, Wry,
|
Wry,
|
||||||
};
|
};
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use super::handle;
|
use super::handle;
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct TrayState {}
|
|
||||||
|
|
||||||
// 托盘点击防抖机制
|
|
||||||
static TRAY_CLICK_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
|
|
||||||
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
|
||||||
|
|
||||||
fn get_tray_click_debounce() -> &'static Mutex<Instant> {
|
|
||||||
TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_handle_tray_click() -> bool {
|
|
||||||
let debounce_lock = get_tray_click_debounce();
|
|
||||||
let mut last_click = debounce_lock.lock();
|
|
||||||
let now = Instant::now();
|
|
||||||
|
|
||||||
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
|
||||||
*last_click = now;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
|
|
||||||
now.duration_since(*last_click).as_millis());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub struct Tray {
|
pub struct Tray {
|
||||||
last_menu_update: Mutex<Option<Instant>>,
|
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
||||||
menu_updating: AtomicBool,
|
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||||
|
is_subscribed: Arc<RwLock<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
pub struct Tray {
|
pub struct Tray {}
|
||||||
last_menu_update: Mutex<Option<Instant>>,
|
|
||||||
menu_updating: AtomicBool,
|
|
||||||
}
|
|
||||||
|
|
||||||
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-new.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-new.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 {
|
impl Tray {
|
||||||
pub fn global() -> &'static Tray {
|
pub fn global() -> &'static Tray {
|
||||||
@@ -173,84 +48,73 @@ impl Tray {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
return TRAY.get_or_init(|| Tray {
|
return TRAY.get_or_init(|| Tray {
|
||||||
last_menu_update: Mutex::new(None),
|
speed_rate: Arc::new(Mutex::new(None)),
|
||||||
menu_updating: AtomicBool::new(false),
|
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||||
|
is_subscribed: Arc::new(RwLock::new(false)),
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
return TRAY.get_or_init(|| Tray {
|
return TRAY.get_or_init(|| Tray {});
|
||||||
last_menu_update: Mutex::new(None),
|
|
||||||
menu_updating: AtomicBool::new(false),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self) -> Result<()> {
|
pub fn init(&self) -> Result<()> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let mut speed_rate = self.speed_rate.lock();
|
||||||
|
*speed_rate = Some(SpeedRate::new());
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新托盘点击行为
|
pub fn create_systray(&self) -> Result<()> {
|
||||||
pub fn update_click_behavior(&self) -> Result<()> {
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
let tray_incon_id = TrayIconId::new("main");
|
||||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
let tray = app_handle.tray_by_id(&tray_incon_id).unwrap();
|
||||||
let tray = app_handle.tray_by_id("main").unwrap();
|
|
||||||
match tray_event.as_str() {
|
#[cfg(target_os = "macos")]
|
||||||
"tray_menu" => tray.set_show_menu_on_left_click(true)?,
|
tray.set_show_menu_on_left_click(false)?;
|
||||||
_ => tray.set_show_menu_on_left_click(false)?,
|
|
||||||
}
|
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::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(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(None),
|
||||||
|
"main_window" => resolve::create_window(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tray.on_menu_event(on_menu_event);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新托盘菜单
|
/// 更新托盘菜单
|
||||||
pub fn update_menu(&self) -> Result<()> {
|
pub fn update_menu(&self) -> Result<()> {
|
||||||
// 调整最小更新间隔,确保状态及时刷新
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
|
|
||||||
|
|
||||||
// 检查是否正在更新
|
|
||||||
if self.menu_updating.load(Ordering::Acquire) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查更新频率,但允许重要事件跳过频率限制
|
|
||||||
let should_force_update = match std::thread::current().name() {
|
|
||||||
Some("main") => true,
|
|
||||||
_ => {
|
|
||||||
let last_update = self.last_menu_update.lock();
|
|
||||||
if let Some(last_time) = *last_update {
|
|
||||||
last_time.elapsed() >= MIN_UPDATE_INTERVAL
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !should_force_update {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let app_handle = match handle::Handle::global().app_handle() {
|
|
||||||
Some(handle) => handle,
|
|
||||||
None => {
|
|
||||||
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 设置更新状态
|
|
||||||
self.menu_updating.store(true, Ordering::Release);
|
|
||||||
|
|
||||||
let result = self.update_menu_internal(&app_handle);
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut last_update = self.last_menu_update.lock();
|
|
||||||
*last_update = Some(Instant::now());
|
|
||||||
}
|
|
||||||
self.menu_updating.store(false, Ordering::Release);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
|
|
||||||
let verge = Config::verge().latest().clone();
|
let verge = Config::verge().latest().clone();
|
||||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
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 tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||||
@@ -266,128 +130,134 @@ impl Tray {
|
|||||||
let profile_uid_and_name = Config::profiles()
|
let profile_uid_and_name = Config::profiles()
|
||||||
.data()
|
.data()
|
||||||
.all_profile_uid_and_name()
|
.all_profile_uid_and_name()
|
||||||
.unwrap_or_default();
|
.unwrap_or(Vec::new());
|
||||||
let is_lightweight_mode = is_in_lightweight_mode();
|
|
||||||
|
|
||||||
match app_handle.tray_by_id("main") {
|
let tray = app_handle.tray_by_id("main").unwrap();
|
||||||
Some(tray) => {
|
let _ = tray.set_menu(Some(create_tray_menu(
|
||||||
let _ = tray.set_menu(Some(create_tray_menu(
|
&app_handle,
|
||||||
app_handle,
|
Some(mode.as_str()),
|
||||||
Some(mode.as_str()),
|
*system_proxy,
|
||||||
*system_proxy,
|
*tun_mode,
|
||||||
*tun_mode,
|
profile_uid_and_name,
|
||||||
profile_uid_and_name,
|
)?));
|
||||||
is_lightweight_mode,
|
Ok(())
|
||||||
)?));
|
|
||||||
log::debug!(target: "app", "托盘菜单更新成功");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新托盘图标
|
/// 更新托盘图标
|
||||||
#[cfg(target_os = "macos")]
|
#[allow(unused_variables)]
|
||||||
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
|
||||||
let app_handle = match handle::Handle::global().app_handle() {
|
|
||||||
Some(handle) => handle,
|
|
||||||
None => {
|
|
||||||
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tray = match app_handle.tray_by_id("main") {
|
|
||||||
Some(tray) => tray,
|
|
||||||
None => {
|
|
||||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (_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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
|
||||||
let is_colorful = colorful == "colorful";
|
|
||||||
|
|
||||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
|
||||||
let _ = tray.set_icon_as_template(!is_colorful);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
|
||||||
let app_handle = match handle::Handle::global().app_handle() {
|
|
||||||
Some(handle) => handle,
|
|
||||||
None => {
|
|
||||||
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tray = match app_handle.tray_by_id("main") {
|
|
||||||
Some(tray) => tray,
|
|
||||||
None => {
|
|
||||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (_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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 更新托盘显示状态的函数
|
|
||||||
pub fn update_tray_display(&self) -> Result<()> {
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let _tray = app_handle.tray_by_id("main").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 common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
|
||||||
self.update_menu()?;
|
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 icon_bytes = if *system_proxy && !*tun_mode {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let mut icon = match tray_icon.as_str() {
|
||||||
|
"colorful" => 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icon
|
||||||
|
} else if *tun_mode {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let mut icon = match tray_icon.as_str() {
|
||||||
|
"colorful" => 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icon
|
||||||
|
} else {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let mut icon = match tray_icon.as_str() {
|
||||||
|
"colorful" => 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icon
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
|
||||||
|
let is_colorful = tray_icon == "colorful";
|
||||||
|
|
||||||
|
// 处理图标和速率
|
||||||
|
let final_icon_bytes = if enable_tray_speed {
|
||||||
|
let rate = rate.or_else(|| {
|
||||||
|
self.speed_rate
|
||||||
|
.lock()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|speed_rate| speed_rate.get_curent_rate())
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用新的方法渲染图标和速率
|
||||||
|
SpeedRate::add_speed_text(icon_bytes, rate)?
|
||||||
|
} else {
|
||||||
|
icon_bytes
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置系统托盘图标
|
||||||
|
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&final_icon_bytes)?));
|
||||||
|
// 只对单色图标使用 template 模式
|
||||||
|
let _ = tray.set_icon_as_template(!is_colorful);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新托盘提示
|
/// 更新托盘提示
|
||||||
pub fn update_tooltip(&self) -> Result<()> {
|
pub fn update_tooltip(&self) -> Result<()> {
|
||||||
let app_handle = match handle::Handle::global().app_handle() {
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
Some(handle) => handle,
|
let version = VERSION.get().unwrap();
|
||||||
None => {
|
|
||||||
log::warn!(target: "app", "更新托盘提示失败: app_handle不存在");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let version = match VERSION.get() {
|
|
||||||
Some(v) => v,
|
|
||||||
None => {
|
|
||||||
log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let verge = Config::verge().latest().clone();
|
let verge = Config::verge().latest().clone();
|
||||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||||
@@ -404,28 +274,23 @@ impl Tray {
|
|||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles();
|
||||||
let profiles = profiles.latest();
|
let profiles = profiles.latest();
|
||||||
if let Some(current_profile_uid) = profiles.get_current() {
|
if let Some(current_profile_uid) = profiles.get_current() {
|
||||||
if let Ok(profile) = profiles.get_item(¤t_profile_uid) {
|
let current_profile = profiles.get_item(¤t_profile_uid);
|
||||||
current_profile_name = match &profile.name {
|
current_profile_name = match ¤t_profile.unwrap().name {
|
||||||
Some(profile_name) => profile_name.to_string(),
|
Some(profile_name) => profile_name.to_string(),
|
||||||
None => current_profile_name,
|
None => current_profile_name,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(tray) = app_handle.tray_by_id("main") {
|
let tray = app_handle.tray_by_id("main").unwrap();
|
||||||
let _ = tray.set_tooltip(Some(&format!(
|
let _ = tray.set_tooltip(Some(&format!(
|
||||||
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||||
t("SysProxy"),
|
t("SysProxy"),
|
||||||
switch_map[system_proxy],
|
switch_map[system_proxy],
|
||||||
t("TUN"),
|
t("TUN"),
|
||||||
switch_map[tun_mode],
|
switch_map[tun_mode],
|
||||||
t("Profile"),
|
t("Profile"),
|
||||||
current_profile_name
|
current_profile_name
|
||||||
)));
|
)));
|
||||||
} else {
|
|
||||||
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,90 +298,75 @@ impl Tray {
|
|||||||
self.update_menu()?;
|
self.update_menu()?;
|
||||||
self.update_icon(None)?;
|
self.update_icon(None)?;
|
||||||
self.update_tooltip()?;
|
self.update_tooltip()?;
|
||||||
// 更新轻量模式显示状态
|
Ok(())
|
||||||
self.update_tray_display()?;
|
}
|
||||||
|
|
||||||
|
/// 订阅流量数据
|
||||||
|
#[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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 取消订阅 traffic 数据
|
/// 取消订阅 traffic 数据
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn unsubscribe_traffic(&self) {}
|
pub fn unsubscribe_traffic(&self) {
|
||||||
|
log::info!(target: "app", "unsubscribe traffic");
|
||||||
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
*self.is_subscribed.write() = false;
|
||||||
log::info!(target: "app", "正在从AppHandle创建系统托盘");
|
if let Some(tx) = self.shutdown_tx.write().take() {
|
||||||
|
drop(tx);
|
||||||
// 获取图标
|
|
||||||
let icon_bytes = TrayState::get_common_tray_icon().1;
|
|
||||||
let icon = tauri::image::Image::from_bytes(&icon_bytes)?;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let builder = TrayIconBuilder::with_id("main")
|
|
||||||
.icon(icon)
|
|
||||||
.icon_as_template(false);
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
let mut builder = TrayIconBuilder::with_id("main")
|
|
||||||
.icon(icon)
|
|
||||||
.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_handle)?;
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
// 添加防抖检查,防止快速连击
|
|
||||||
if !should_handle_tray_click() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match tray_event.as_str() {
|
|
||||||
"system_proxy" => feat::toggle_system_proxy(),
|
|
||||||
"tun_mode" => feat::toggle_tun_mode(None),
|
|
||||||
"main_window" => {
|
|
||||||
use crate::utils::window_manager::WindowManager;
|
|
||||||
log::info!(target: "app", "Tray点击事件: 显示主窗口");
|
|
||||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
|
||||||
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
|
|
||||||
crate::module::lightweight::exit_lightweight_mode();
|
|
||||||
}
|
|
||||||
let result = WindowManager::show_main_window();
|
|
||||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
tray.on_menu_event(on_menu_event);
|
|
||||||
log::info!(target: "app", "系统托盘创建成功");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 托盘统一的状态更新函数
|
|
||||||
pub fn update_all_states(&self) -> Result<()> {
|
|
||||||
// 确保所有状态更新完成
|
|
||||||
self.update_menu()?;
|
|
||||||
self.update_icon(None)?;
|
|
||||||
self.update_tooltip()?;
|
|
||||||
self.update_tray_display()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,13 +376,9 @@ fn create_tray_menu(
|
|||||||
system_proxy_enabled: bool,
|
system_proxy_enabled: bool,
|
||||||
tun_mode_enabled: bool,
|
tun_mode_enabled: bool,
|
||||||
profile_uid_and_name: Vec<(String, String)>,
|
profile_uid_and_name: Vec<(String, String)>,
|
||||||
is_lightweight_mode: bool,
|
|
||||||
) -> Result<tauri::menu::Menu<Wry>> {
|
) -> Result<tauri::menu::Menu<Wry>> {
|
||||||
let mode = mode.unwrap_or("");
|
let mode = mode.unwrap_or("");
|
||||||
|
let version = VERSION.get().unwrap();
|
||||||
let unknown_version = String::from("unknown");
|
|
||||||
let version = VERSION.get().unwrap_or(&unknown_version);
|
|
||||||
|
|
||||||
let hotkeys = Config::verge()
|
let hotkeys = Config::verge()
|
||||||
.latest()
|
.latest()
|
||||||
.hotkeys
|
.hotkeys
|
||||||
@@ -549,24 +395,21 @@ fn create_tray_menu(
|
|||||||
.collect::<std::collections::HashMap<String, String>>()
|
.collect::<std::collections::HashMap<String, String>>()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
|
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(profile_uid, profile_name)| {
|
.map(|(profile_uid, profile_name)| {
|
||||||
let is_current_profile = Config::profiles()
|
let is_current_profile = Config::profiles().data().is_current_profile_index(profile_uid.to_string());
|
||||||
.data()
|
CheckMenuItem::with_id(
|
||||||
.is_current_profile_index(profile_uid.to_string());
|
app_handle,
|
||||||
CheckMenuItem::with_id(
|
&format!("profiles_{}", profile_uid),
|
||||||
app_handle,
|
t(&profile_name),
|
||||||
format!("profiles_{profile_uid}"),
|
true,
|
||||||
t(profile_name),
|
is_current_profile,
|
||||||
true,
|
None::<&str>,
|
||||||
is_current_profile,
|
)
|
||||||
None::<&str>,
|
.unwrap()
|
||||||
)
|
}).collect();
|
||||||
.unwrap()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||||
@@ -612,13 +455,12 @@ fn create_tray_menu(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let profiles = &Submenu::with_id_and_items(
|
let profiles = &Submenu::with_id_and_items(
|
||||||
app_handle,
|
app_handle,
|
||||||
"profiles",
|
"profiles",
|
||||||
t("Profiles"),
|
t("Profiles"),
|
||||||
true,
|
true,
|
||||||
&profile_menu_items,
|
&profile_menu_items,
|
||||||
)
|
).unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let system_proxy = &CheckMenuItem::with_id(
|
let system_proxy = &CheckMenuItem::with_id(
|
||||||
app_handle,
|
app_handle,
|
||||||
@@ -640,16 +482,6 @@ fn create_tray_menu(
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let lighteweight_mode = &CheckMenuItem::with_id(
|
|
||||||
app_handle,
|
|
||||||
"entry_lightweight_mode",
|
|
||||||
t("LightWeight Mode"),
|
|
||||||
true,
|
|
||||||
is_lightweight_mode,
|
|
||||||
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let copy_env =
|
let copy_env =
|
||||||
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
||||||
|
|
||||||
@@ -742,8 +574,6 @@ fn create_tray_menu(
|
|||||||
separator,
|
separator,
|
||||||
system_proxy,
|
system_proxy,
|
||||||
tun_mode,
|
tun_mode,
|
||||||
separator,
|
|
||||||
lighteweight_mode,
|
|
||||||
copy_env,
|
copy_env,
|
||||||
open_dir,
|
open_dir,
|
||||||
more,
|
more,
|
||||||
@@ -759,77 +589,26 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
match event.id.as_ref() {
|
match event.id.as_ref() {
|
||||||
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
||||||
let mode = &mode[0..mode.len() - 5];
|
let mode = &mode[0..mode.len() - 5];
|
||||||
logging!(
|
println!("change mode to: {}", mode);
|
||||||
info,
|
|
||||||
Type::ProxyMode,
|
|
||||||
true,
|
|
||||||
"Switch Proxy Mode To: {}",
|
|
||||||
mode
|
|
||||||
);
|
|
||||||
feat::change_clash_mode(mode.into());
|
feat::change_clash_mode(mode.into());
|
||||||
}
|
}
|
||||||
"open_window" => {
|
"open_window" => resolve::create_window(),
|
||||||
use crate::utils::window_manager::WindowManager;
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
"tun_mode" => feat::toggle_tun_mode(None),
|
||||||
|
|
||||||
if !should_handle_tray_click() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
|
||||||
log::info!(target: "app", "当前在轻量模式,正在退出");
|
|
||||||
crate::module::lightweight::exit_lightweight_mode();
|
|
||||||
}
|
|
||||||
let result = WindowManager::show_main_window();
|
|
||||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
|
||||||
}
|
|
||||||
"system_proxy" => {
|
|
||||||
feat::toggle_system_proxy();
|
|
||||||
}
|
|
||||||
"tun_mode" => {
|
|
||||||
feat::toggle_tun_mode(None);
|
|
||||||
}
|
|
||||||
"copy_env" => feat::copy_clash_env(),
|
"copy_env" => feat::copy_clash_env(),
|
||||||
"open_app_dir" => {
|
"open_app_dir" => crate::log_err!(cmd::open_app_dir()),
|
||||||
let _ = cmd::open_app_dir();
|
"open_core_dir" => crate::log_err!(cmd::open_core_dir()),
|
||||||
}
|
"open_logs_dir" => crate::log_err!(cmd::open_logs_dir()),
|
||||||
"open_core_dir" => {
|
|
||||||
let _ = cmd::open_core_dir();
|
|
||||||
}
|
|
||||||
"open_logs_dir" => {
|
|
||||||
let _ = cmd::open_logs_dir();
|
|
||||||
}
|
|
||||||
"restart_clash" => feat::restart_clash_core(),
|
"restart_clash" => feat::restart_clash_core(),
|
||||||
"restart_app" => feat::restart_app(),
|
"restart_app" => feat::restart_app(),
|
||||||
"entry_lightweight_mode" => {
|
|
||||||
if !should_handle_tray_click() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
|
|
||||||
if was_lightweight {
|
|
||||||
crate::module::lightweight::exit_lightweight_mode();
|
|
||||||
} else {
|
|
||||||
crate::module::lightweight::entry_lightweight_mode();
|
|
||||||
}
|
|
||||||
|
|
||||||
if was_lightweight {
|
|
||||||
use crate::utils::window_manager::WindowManager;
|
|
||||||
let result = WindowManager::show_main_window();
|
|
||||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"quit" => {
|
"quit" => {
|
||||||
feat::quit();
|
println!("quit");
|
||||||
}
|
feat::quit(Some(0));
|
||||||
|
},
|
||||||
id if id.starts_with("profiles_") => {
|
id if id.starts_with("profiles_") => {
|
||||||
let profile_index = &id["profiles_".len()..];
|
let profile_index = &id["profiles_".len()..];
|
||||||
feat::toggle_proxy_profile(profile_index.into());
|
feat::toggle_proxy_profile(profile_index.into());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = Tray::global().update_all_states() {
|
|
||||||
log::warn!(target: "app", "更新托盘状态失败: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,239 @@
|
|||||||
|
use crate::module::mihomo::Rate;
|
||||||
|
use crate::module::mihomo::MihomoManager;
|
||||||
|
use crate::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;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio_tungstenite::tungstenite::http;
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
use tungstenite::client::IntoClientRequest;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SpeedRate {
|
||||||
|
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||||
|
last_update: Arc<Mutex<std::time::Instant>>,
|
||||||
|
// 移除 base_image,不再缓存原始图像
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 如果速率变化不大(小于10%),则不更新
|
||||||
|
let should_update = {
|
||||||
|
let up_change = (current.up as f64 - up as f64).abs() / (current.up as f64 + 1.0);
|
||||||
|
let down_change =
|
||||||
|
(current.down as f64 - down as f64).abs() / (current.down as f64 + 1.0);
|
||||||
|
up_change > 0.1 || down_change > 0.1
|
||||||
|
};
|
||||||
|
|
||||||
|
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(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
||||||
|
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
|
||||||
|
|
||||||
|
// 加载原始图标
|
||||||
|
let icon_image = image::load_from_memory(&icon_bytes)?;
|
||||||
|
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
|
||||||
|
|
||||||
|
// 判断是否为彩色图标
|
||||||
|
let is_colorful =
|
||||||
|
!crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
||||||
|
|
||||||
|
// 增加文本宽度和间距
|
||||||
|
let text_width = 580; // 文本区域宽度
|
||||||
|
let total_width = icon_width + text_width;
|
||||||
|
|
||||||
|
// 创建新的透明画布
|
||||||
|
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||||
|
|
||||||
|
// 将原始图标绘制到新画布的左侧
|
||||||
|
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 (text_color, shadow_color) = if is_colorful {
|
||||||
|
// 彩色图标使用黑色文本和轻微白色阴影
|
||||||
|
(
|
||||||
|
Rgba([255u8, 255u8, 255u8, 255u8]),
|
||||||
|
Rgba([0u8, 0u8, 0u8, 160u8]),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 单色图标使用白色文本和轻微黑色阴影
|
||||||
|
(
|
||||||
|
Rgba([255u8, 255u8, 255u8, 255u8]),
|
||||||
|
Rgba([0u8, 0u8, 0u8, 120u8]),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
// 减小字体大小以适应文本区域
|
||||||
|
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_bytes_speed(rate.up);
|
||||||
|
let down_text = format_bytes_speed(rate.down);
|
||||||
|
|
||||||
|
// 计算文本位置,确保垂直间距合适
|
||||||
|
// 修改文本位置为居右显示
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
14
src-tauri/src/crate_mihomo_api/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "mihomo_api"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
debug = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
reqwest = { version = "0.12.12", features = ["json"] }
|
||||||
|
serde = { version = "1.0.218", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.43.0", features = ["rt", "macros"] }
|
||||||
134
src-tauri/src/crate_mihomo_api/src/lib.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use reqwest::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: 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: &str,
|
||||||
|
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(2))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.request(
|
||||||
|
match method {
|
||||||
|
"GET" => reqwest::Method::GET,
|
||||||
|
"PUT" => reqwest::Method::PUT,
|
||||||
|
"POST" => reqwest::Method::POST,
|
||||||
|
"PATCH" => reqwest::Method::PATCH,
|
||||||
|
_ => reqwest::Method::GET,
|
||||||
|
},
|
||||||
|
&url,
|
||||||
|
)
|
||||||
|
.json(&data.unwrap_or(json!({})))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let response = if method != "PUT" {
|
||||||
|
client_response.json::<serde_json::Value>().await
|
||||||
|
} else {
|
||||||
|
client_response.text().await.map(|text| json!(text))
|
||||||
|
}
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_proxies(&self) -> Result<&Self, String> {
|
||||||
|
let url = format!("{}/proxies", self.mihomo_server);
|
||||||
|
let proxies = self.send_request("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("GET", url, None).await?;
|
||||||
|
self.update_providers_proxies(providers_proxies);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MihomoManager {
|
||||||
|
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("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("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("GET", url, None).await?;
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src-tauri/src/crate_mihomo_api/src/model.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
29
src-tauri/src/crate_mihomo_api/tests/test_mihomo_api.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -5,10 +5,17 @@ mod script;
|
|||||||
pub mod seq;
|
pub mod seq;
|
||||||
mod tun;
|
mod tun;
|
||||||
|
|
||||||
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
|
use self::chain::*;
|
||||||
use crate::{config::Config, utils::tmpl};
|
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 serde_yaml::Mapping;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
type ResultLog = Vec<(String, String)>;
|
type ResultLog = Vec<(String, String)>;
|
||||||
|
|
||||||
@@ -22,7 +29,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge = verge.latest();
|
let verge = verge.latest();
|
||||||
(
|
(
|
||||||
Some(verge.get_valid_clash_core()),
|
verge.clash_core.clone(),
|
||||||
verge.enable_tun_mode.unwrap_or(false),
|
verge.enable_tun_mode.unwrap_or(false),
|
||||||
verge.enable_builtin_enhanced.unwrap_or(true),
|
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||||
verge.verge_socks_enabled.unwrap_or(false),
|
verge.verge_socks_enabled.unwrap_or(false),
|
||||||
@@ -260,30 +267,17 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
if enable_dns_settings {
|
if enable_dns_settings {
|
||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
// 尝试读取dns_config.yaml
|
||||||
if let Ok(app_dir) = dirs::app_home_dir() {
|
if let Ok(app_dir) = dirs::app_home_dir() {
|
||||||
let dns_path = app_dir.join("dns_config.yaml");
|
let dns_path = app_dir.join("dns_config.yaml");
|
||||||
|
|
||||||
if dns_path.exists() {
|
if dns_path.exists() {
|
||||||
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
|
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) {
|
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
||||||
// 处理hosts配置
|
// 将DNS配置合并到最终配置中
|
||||||
if let Some(hosts_value) = dns_config.get("hosts") {
|
config.insert("dns".into(), dns_config.into());
|
||||||
if hosts_value.is_mapping() {
|
log::info!(target: "app", "apply dns_config.yaml");
|
||||||
config.insert("hosts".into(), hosts_value.clone());
|
|
||||||
log::info!(target: "app", "apply hosts configuration");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dns_value) = dns_config.get("dns") {
|
|
||||||
if let Some(dns_mapping) = dns_value.as_mapping() {
|
|
||||||
config.insert("dns".into(), dns_mapping.clone().into());
|
|
||||||
log::info!(target: "app", "apply dns_config.yaml (dns section)");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config.insert("dns".into(), dns_config.into());
|
|
||||||
log::info!(target: "app", "apply dns_config.yaml");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,40 +33,37 @@ pub fn use_script(
|
|||||||
}
|
}
|
||||||
let _ = context.eval(Source::from_bytes(
|
let _ = context.eval(Source::from_bytes(
|
||||||
r#"var console = Object.freeze({
|
r#"var console = Object.freeze({
|
||||||
log(data){__verge_log__("log",JSON.stringify(data, null, 2))},
|
log(data){__verge_log__("log",JSON.stringify(data))},
|
||||||
info(data){__verge_log__("info",JSON.stringify(data, null, 2))},
|
info(data){__verge_log__("info",JSON.stringify(data))},
|
||||||
error(data){__verge_log__("error",JSON.stringify(data, null, 2))},
|
error(data){__verge_log__("error",JSON.stringify(data))},
|
||||||
debug(data){__verge_log__("debug",JSON.stringify(data, null, 2))},
|
debug(data){__verge_log__("debug",JSON.stringify(data))},
|
||||||
warn(data){__verge_log__("warn",JSON.stringify(data, null, 2))},
|
|
||||||
table(data){__verge_log__("table",JSON.stringify(data, null, 2))},
|
|
||||||
});"#,
|
});"#,
|
||||||
));
|
));
|
||||||
|
|
||||||
let config = use_lowercase(config.clone());
|
let config = use_lowercase(config.clone());
|
||||||
let config_str = serde_json::to_string(&config)?;
|
let config_str = serde_json::to_string(&config)?;
|
||||||
|
|
||||||
// 仅处理 name 参数中的特殊字符
|
|
||||||
let safe_name = escape_js_string_for_single_quote(&name);
|
|
||||||
|
|
||||||
let code = format!(
|
let code = format!(
|
||||||
r#"try{{
|
r#"try{{
|
||||||
{script};
|
{script};
|
||||||
JSON.stringify(main({config_str},'{safe_name}')||'')
|
JSON.stringify(main({config_str},'{name}')||'')
|
||||||
}} catch(err) {{
|
}} catch(err) {{
|
||||||
`__error_flag__ ${{err.toString()}}`
|
`__error_flag__ ${{err.toString()}}`
|
||||||
}}"#
|
}}"#
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
|
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
|
||||||
if !result.is_string() {
|
if !result.is_string() {
|
||||||
anyhow::bail!("main function should return object");
|
anyhow::bail!("main function should return object");
|
||||||
}
|
}
|
||||||
let result = result.to_string(&mut context).unwrap();
|
let result = result.to_string(&mut context).unwrap();
|
||||||
let result = result.to_std_string().unwrap();
|
let result = result.to_std_string().unwrap();
|
||||||
|
if result.starts_with("__error_flag__") {
|
||||||
// 直接解析JSON结果,不做其他解析
|
anyhow::bail!(result[15..].to_owned());
|
||||||
let res: Result<Mapping, Error> = parse_json_safely(&result);
|
}
|
||||||
|
if result == "\"\"" {
|
||||||
|
anyhow::bail!("main function should return object");
|
||||||
|
}
|
||||||
|
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
||||||
let mut out = outputs.lock().unwrap();
|
let mut out = outputs.lock().unwrap();
|
||||||
match res {
|
match res {
|
||||||
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||||
@@ -80,27 +77,6 @@ pub fn use_script(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_json_safely(json_str: &str) -> Result<Mapping, Error> {
|
|
||||||
let json_str = strip_outer_quotes(json_str);
|
|
||||||
|
|
||||||
Ok(serde_json::from_str::<Mapping>(json_str)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除字符串外层的引号
|
|
||||||
fn strip_outer_quotes(s: &str) -> &str {
|
|
||||||
let s = s.trim();
|
|
||||||
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
|
|
||||||
&s[1..s.len() - 1]
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转义单引号和反斜杠,用于单引号包裹的JavaScript字符串
|
|
||||||
fn escape_js_string_for_single_quote(s: &str) -> String {
|
|
||||||
s.replace('\\', "\\\\").replace('\'', "\\'")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_script() {
|
fn test_script() {
|
||||||
let script = r#"
|
let script = r#"
|
||||||
@@ -128,31 +104,6 @@ fn test_script() {
|
|||||||
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
|
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
|
||||||
|
|
||||||
let _ = serde_yaml::to_string(&config).unwrap();
|
let _ = serde_yaml::to_string(&config).unwrap();
|
||||||
let yaml_config_size = std::mem::size_of_val(&config);
|
|
||||||
dbg!(yaml_config_size);
|
|
||||||
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
|
|
||||||
dbg!(box_yaml_config_size);
|
|
||||||
dbg!(results);
|
dbg!(results);
|
||||||
assert!(box_yaml_config_size < yaml_config_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试特殊字符转义功能
|
|
||||||
#[test]
|
|
||||||
fn test_escape_unescape() {
|
|
||||||
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
|
||||||
let escaped = escape_js_string_for_single_quote(test_string);
|
|
||||||
println!("Original: {}", test_string);
|
|
||||||
println!("Escaped: {}", escaped);
|
|
||||||
|
|
||||||
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
|
||||||
let parsed = parse_json_safely(json_str).unwrap();
|
|
||||||
|
|
||||||
assert!(parsed.contains_key("key"));
|
|
||||||
assert!(parsed.contains_key("nested"));
|
|
||||||
|
|
||||||
let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#;
|
|
||||||
let parsed_quoted = parse_json_safely(quoted_json_str).unwrap();
|
|
||||||
|
|
||||||
assert!(parsed_quoted.contains_key("key"));
|
|
||||||
assert!(parsed_quoted.contains_key("nested"));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ proxy-groups:
|
|||||||
- "proxy1"
|
- "proxy1"
|
||||||
"#;
|
"#;
|
||||||
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
|
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
|
||||||
|
|
||||||
let seq = SeqMap {
|
let seq = SeqMap {
|
||||||
prepend: Sequence::new(),
|
prepend: Sequence::new(),
|
||||||
append: Sequence::new(),
|
append: Sequence::new(),
|
||||||
@@ -121,32 +121,16 @@ proxy-groups:
|
|||||||
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
|
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
|
||||||
assert_eq!(proxies.len(), 1);
|
assert_eq!(proxies.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
proxies[0]
|
proxies[0].as_mapping().unwrap().get("name").unwrap().as_str().unwrap(),
|
||||||
.as_mapping()
|
|
||||||
.unwrap()
|
|
||||||
.get("name")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap(),
|
|
||||||
"proxy2"
|
"proxy2"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if proxy1 is removed from all groups
|
// Check if proxy1 is removed from all groups
|
||||||
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
|
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
|
||||||
let group1_proxies = groups[0]
|
let group1_proxies = groups[0].as_mapping().unwrap()
|
||||||
.as_mapping()
|
.get("proxies").unwrap().as_sequence().unwrap();
|
||||||
.unwrap()
|
let group2_proxies = groups[1].as_mapping().unwrap()
|
||||||
.get("proxies")
|
.get("proxies").unwrap().as_sequence().unwrap();
|
||||||
.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.len(), 1);
|
||||||
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
|
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
|
||||||
|
|||||||