Compare commits
822 Commits
v4.2.4
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e516d1d866 | ||
|
|
f4bcb73fe9 | ||
|
|
993524c4dd | ||
|
|
efa9f28aad | ||
|
|
4beed5048a | ||
|
|
68b1ed4238 | ||
|
|
e5b43a9c45 | ||
|
|
98a2b0f176 | ||
|
|
06770eb3cc | ||
|
|
dfc403733f | ||
|
|
f423f3b577 | ||
| fcd8f042b7 | |||
|
|
3140c29832 | ||
|
|
1c97228bc0 | ||
|
|
8ce747a839 | ||
|
|
124d3e34d6 | ||
| 837af62a60 | |||
| 14a8a308c5 | |||
| 28ce06c92d | |||
| 2d6dae3fc9 | |||
| d2066e9631 | |||
| 13caed7207 | |||
| 2c57426051 | |||
| bb602d92cf | |||
| 57a3505cab | |||
|
|
2ab402e8ff | ||
|
|
40d06cce54 | ||
|
|
7b0307070e | ||
|
|
09038aeb09 | ||
|
|
f869c19353 | ||
|
|
b6fd72f0d8 | ||
|
|
f9192ef9eb | ||
|
|
6f77c29e19 | ||
|
|
4e44669695 | ||
|
|
f04b7bf073 | ||
|
|
0ac0225d7d | ||
|
|
128cf3de4c | ||
|
|
e528936c8d | ||
|
|
eed0319098 | ||
| 74ea0af9bc | |||
| 69fac94058 | |||
| 29bd8a4aff | |||
|
|
54dc1d392d | ||
|
|
5c55db7067 | ||
|
|
bbe5286601 | ||
|
|
06b3147b53 | ||
|
|
dbd1731d7f | ||
|
|
320d7ab17d | ||
|
|
4ad346ed14 | ||
|
|
f9f8b56d18 | ||
| 2101ae9908 | |||
| dbc5784691 | |||
|
|
632ad33562 | ||
|
|
d3639d44e1 | ||
| af28869d2a | |||
|
|
34df37c040 | ||
| bd58a512c9 | |||
|
|
cf4c8cfe17 | ||
|
|
ff8f501b98 | ||
|
|
59de0ec510 | ||
|
|
cda50ea134 | ||
|
|
97ec518e43 | ||
| d338210426 | |||
| b3fe49b8d1 | |||
| 96c022c2ec | |||
|
|
0f1cbfc82d | ||
|
|
53d91fb3f8 | ||
|
|
7e74af93a0 | ||
| 2331d7e38b | |||
| 6fd46087bb | |||
|
|
76f330e4d3 | ||
|
|
1a541cbc63 | ||
|
|
133ab0e78c | ||
|
|
35aa3cc42d | ||
|
|
4114e4ffc0 | ||
|
|
22ebe7e51b | ||
| 0437778c14 | |||
| 70fc623f54 | |||
|
|
32df65fb65 | ||
|
|
4f1e49ce85 | ||
|
|
db0ac79e44 | ||
|
|
d5331b728d | ||
| 1c640c3df8 | |||
| de45255feb | |||
|
|
1f89639463 | ||
|
|
cb741c7b2e | ||
| 6897f35d1e | |||
| 7c34b3ca94 | |||
|
|
74a72961f7 | ||
|
|
981c3a8624 | ||
|
|
a16f9b2da4 | ||
|
|
1f85360c92 | ||
|
|
5f7cdd6a57 | ||
|
|
887f10ef3f | ||
|
|
14c61815c6 | ||
|
|
4abce146a9 | ||
|
|
0e8094e52f | ||
|
|
f3680a8274 | ||
|
|
cd706640d9 | ||
|
|
e0ba88d7b0 | ||
|
|
4cc1d1186e | ||
|
|
5eab1b0986 | ||
|
|
60e8ac0ce9 | ||
|
|
a5e09bc489 | ||
|
|
199907eb26 | ||
|
|
969e223fb4 | ||
|
|
e62b9b3943 | ||
|
|
a9769c6397 | ||
|
|
d21d2f9a1d | ||
|
|
bd6d7e8189 | ||
|
|
c0af9d7de0 | ||
| e569930287 | |||
| 5cd0ca29a4 | |||
| b3bb43dcdf | |||
| 01ee70f3fb | |||
|
|
ed6b52451b | ||
|
|
6150142d62 | ||
|
|
bf99e41cee | ||
|
|
4db49e0bb9 | ||
|
|
f559a71de2 | ||
|
|
b5b208c611 | ||
| 7da7a8b4f7 | |||
|
|
010d99ce78 | ||
|
|
9946a8376a | ||
|
|
b11550c500 | ||
|
|
1e8ada6c36 | ||
|
|
aacf32ac61 | ||
|
|
308bb83bec | ||
|
|
4dff160b82 | ||
|
|
cd1d114cac | ||
|
|
f36d647594 | ||
|
|
edadc6f7ce | ||
|
|
1585ab564d | ||
|
|
6c52d5a2f0 | ||
|
|
63fd3d590b | ||
|
|
139fd0bcc3 | ||
|
|
481a204a19 | ||
|
|
62877257e4 | ||
| 2a493bd62a | |||
| 249e8c1a08 | |||
| 2d44389587 | |||
| 84ec172871 | |||
| ac9418f787 | |||
|
|
e79830565e | ||
|
|
4298961311 | ||
|
|
cec8944192 | ||
|
|
6b78162784 | ||
|
|
7ca0dcc918 | ||
|
|
804c3ed62c | ||
|
|
aaee9aa1ba | ||
| 660b82da7a | |||
|
|
d0d5d76f10 | ||
|
|
4026b35636 | ||
| 2558cfca75 | |||
| 4b4aa8ba84 | |||
|
|
1ae2a9bb87 | ||
|
|
3b9d27383b | ||
|
|
17d7e3bad0 | ||
|
|
8ea6c9565e | ||
|
|
67b327e192 | ||
|
|
f98a749efa | ||
|
|
9d067f1c6a | ||
|
|
d1fdd5f672 | ||
|
|
76fcc13d4a | ||
|
|
76b32ad2c1 | ||
|
|
ad56904e56 | ||
| 5f12ddf332 | |||
| 4d8d71f294 | |||
| a15b2bd8ce | |||
| 820649225d | |||
|
|
d0651e2104 | ||
|
|
8910e09493 | ||
|
|
eafd6eb808 | ||
|
|
e07c1a844a | ||
|
|
6846415899 | ||
|
|
4e2419a5f1 | ||
|
|
1866495379 | ||
|
|
c4d1ed3184 | ||
|
|
aa6836d26d | ||
| e49d67f501 | |||
|
|
1ab865df7b | ||
|
|
b36fb89566 | ||
| 94dea445cf | |||
| c8380ddb90 | |||
|
|
6c7a0226fd | ||
|
|
79bd982383 | ||
|
|
e097e40826 | ||
|
|
642b34eca3 | ||
| fbd7fa6a00 | |||
|
|
19537698eb | ||
| 805783c85b | |||
| fd12a6359d | |||
| 04158a40bc | |||
|
|
a10c927253 | ||
|
|
b16bf0fe3f | ||
|
|
47e7291885 | ||
|
|
a034f10a19 | ||
|
|
80eb0bc49d | ||
|
|
90b8020c0a | ||
|
|
a33e2675dd | ||
|
|
ed81cc65ce | ||
|
|
fd8f602da2 | ||
| 4b4be6fa0f | |||
|
|
ab48b91f26 | ||
|
|
8ae2e82d18 | ||
|
|
d43d96e8eb | ||
|
|
d207c65df7 | ||
| 6138fc47b1 | |||
| d9aa6305da | |||
|
|
80aa2873f7 | ||
|
|
3b7b3b8f74 | ||
|
|
b4e4c97cbf | ||
|
|
e3c0add012 | ||
| 7237523237 | |||
| 81d9d3d66c | |||
| 86f8884e45 | |||
|
|
88bc883fe1 | ||
| 482077c862 | |||
|
|
5c4fd4024b | ||
| 75ff858f68 | |||
| 64fb077d65 | |||
|
|
d2e45038fd | ||
|
|
e3c1143c0c | ||
| fb4dc7fa83 | |||
|
|
3490eff76e | ||
|
|
54cdef691a | ||
|
|
4f8c12292b | ||
|
|
1fd357a49c | ||
|
|
e144d62cdb | ||
|
|
a4472b4f9c | ||
|
|
8de3405aa5 | ||
|
|
afd450eee6 | ||
|
|
6798d896bc | ||
|
|
b38c81b08c | ||
| 364e7a273f | |||
| a0c5334f57 | |||
|
|
41bb159542 | ||
|
|
706bb8584d | ||
|
|
a43b12bc17 | ||
|
|
cbba670eb2 | ||
|
|
ac182a7c77 | ||
|
|
9c4e8d256a | ||
|
|
4a8fa68632 | ||
|
|
6bf30b4dc6 | ||
| 2636858fc0 | |||
| 250cccc0b9 | |||
|
|
f15ed86852 | ||
|
|
33a2cee5ef | ||
|
|
bd94a26a15 | ||
|
|
02788d3bfc | ||
| fc8afe6624 | |||
|
|
93a61232cf | ||
|
|
c4cd53277f | ||
|
|
f3e5a03a0f | ||
|
|
bf62c25c93 | ||
| b9a7b6a889 | |||
|
|
1ffd8c1a5a | ||
|
|
58bd4a1765 | ||
|
|
278800c077 | ||
|
|
8142c0398f | ||
|
|
5e2224dfdb | ||
|
|
a206bb9e5c | ||
|
|
79feb73b27 | ||
|
|
97678368e1 | ||
|
|
678e09e536 | ||
|
|
377e5250f9 | ||
| 003f150a74 | |||
|
|
54e289ce56 | ||
| f26d336195 | |||
|
|
4fa7313b4f | ||
|
|
a0a2998fcb | ||
|
|
5007939d1c | ||
|
|
c10d39ba83 | ||
|
|
d0e0af9ed7 | ||
|
|
1a4a6fd5a3 | ||
|
|
f1c3818249 | ||
|
|
eabd8b21a1 | ||
|
|
09a294b38f | ||
| 949044fcdb | |||
| 272fd9b425 | |||
| 6787b015c5 | |||
|
|
8a21b3da9b | ||
| 1473fa64ed | |||
| 8eb27555f5 | |||
| 05c90689e5 | |||
| 07adbfd68c | |||
| 375cf3286a | |||
| 2fec029cb7 | |||
| 0f4ea4786f | |||
|
|
d74e42c281 | ||
|
|
b926880ae9 | ||
|
|
ffd13e8362 | ||
| baf8a642b8 | |||
| 6b00c8a6be | |||
| f4fe4ec019 | |||
| 17cc1fa8a3 | |||
| c91cd55586 | |||
| 24b9dc98d0 | |||
| 909af77649 | |||
| c8b1bf6d08 | |||
| 7f622de857 | |||
| b586e311be | |||
| 11e3c2281e | |||
| a40fa37bfd | |||
| 0162e1a9b7 | |||
| 68df1d5332 | |||
|
|
bd8a26a5ae | ||
|
|
4125c93267 | ||
| b4ebf2ccc1 | |||
| 6941850d90 | |||
| 329ebd8633 | |||
|
|
2ac631a18d | ||
|
|
0e47461c3c | ||
| 1d02894957 | |||
| 76aff5d627 | |||
|
|
42710ecf60 | ||
|
|
e98ebe3e84 | ||
|
|
716d864343 | ||
| 432ec7b9b1 | |||
| 7b74098a09 | |||
|
|
39c8db0846 | ||
|
|
919f273970 | ||
|
|
dd0fc19f5d | ||
| ed3edd5eff | |||
|
|
f45aff61fd | ||
| e4cbbbcdea | |||
|
|
88006ec5f4 | ||
| 49821abc12 | |||
|
|
ee8075e4b8 | ||
| f41f066abc | |||
| 7575909bad | |||
| 8d044e684a | |||
| 88374e5da4 | |||
| c057c2ae21 | |||
| c5fd0c1253 | |||
| fe35e37371 | |||
| fab4645132 | |||
| 5d87a86a33 | |||
| f281ceb5ef | |||
| 2a77e33206 | |||
|
|
fe3fe6398d | ||
| f6c1e591f0 | |||
| f28b4cbad0 | |||
|
|
f62740f20b | ||
| c15c74895b | |||
| 7db6fcbcc8 | |||
| 7fe2ae2acc | |||
| ab7afb3d3b | |||
|
|
389e06cf9e | ||
|
|
05abc0e678 | ||
|
|
1fa9cbeb7a | ||
|
|
928746fe88 | ||
| 96ef72d300 | |||
| 9f6c86dbbc | |||
| d70112216f | |||
|
|
e26ca8e81a | ||
|
|
b5df09cfd2 | ||
|
|
163cb78fc9 | ||
|
|
bce9777eae | ||
|
|
af402330b2 | ||
|
|
fbcc149849 | ||
|
|
4c2a6407a1 | ||
|
|
331e0b55ee | ||
|
|
da0b379b69 | ||
|
|
18a145b69b | ||
| 464758073b | |||
|
|
dea5f3db9a | ||
| fdb724ae8b | |||
| f7ac9daaac | |||
| 183e35ac97 | |||
| 2f87d79713 | |||
| 3e0a9f3a94 | |||
|
|
52fbd9225d | ||
| dc27127322 | |||
|
|
92a4ed9529 | ||
|
|
275e9e6bcc | ||
|
|
94ce37a948 | ||
|
|
998aaffd70 | ||
|
|
515367f61f | ||
|
|
8e2c6bb642 | ||
| 08daef4dcd | |||
|
|
1fdac22bea | ||
| 2f182519a5 | |||
| b30073cb80 | |||
| b3a15de00b | |||
|
|
22a8cdb8d8 | ||
|
|
9acec97257 | ||
| 32ddeef6f0 | |||
| a646578128 | |||
| bf9b911cb2 | |||
|
|
7ece8da1db | ||
| a040dfc4ef | |||
| 7cc754f8b9 | |||
| 71a5966700 | |||
| a738f102a6 | |||
| 9f849608db | |||
|
|
7a253effa6 | ||
|
|
3c6c776828 | ||
|
|
2326cfcaa3 | ||
|
|
1f5cf3acff | ||
|
|
829cac0f6a | ||
|
|
b18ad0fefa | ||
|
|
2391e5b806 | ||
| 68616fe75a | |||
|
|
512667b850 | ||
|
|
6765fbb36e | ||
|
|
9732a3e65f | ||
| 2237dba3c5 | |||
| 95126a85d8 | |||
|
|
13e9d248e8 | ||
| 9aeda23ade | |||
| 16ede80fd4 | |||
| 7d728cb3ae | |||
|
|
bb84099072 | ||
|
|
bec82534f1 | ||
|
|
9c73dccd1e | ||
| 9fb25a2d33 | |||
|
|
175efdefde | ||
|
|
ff78f8eb27 | ||
|
|
df7006dc1e | ||
| bc6ae5562e | |||
|
|
46ee99c5f2 | ||
| c4dde028b2 | |||
|
|
b543fedfaa | ||
|
|
09371f0a5c | ||
| b4665309b9 | |||
| 218784a8de | |||
| 16ab7fc176 | |||
| 85cd830046 | |||
| 76a6f8c33e | |||
| da9aa71c20 | |||
| bf12f1f29a | |||
|
|
91209ad9e2 | ||
|
|
6b729be34e | ||
|
|
409a7a2d03 | ||
| 679c695700 | |||
|
|
135555e3ea | ||
|
|
9a87a62353 | ||
| 1b94986e9a | |||
| c8d3425293 | |||
| 0a49d45160 | |||
| 0171c3ca4d | |||
| e7f898f357 | |||
|
|
ba1fcd1f26 | ||
|
|
7941f5cafd | ||
| 6aca142696 | |||
| f85a3024ef | |||
| 161dc478ae | |||
| ae151a9311 | |||
|
|
8cbd542a75 | ||
| 18202045bf | |||
| 89bf0cbad7 | |||
| 4ff041854c | |||
|
|
b5c118eee5 | ||
| be2c446906 | |||
| 5c57841cd9 | |||
| 40ca642c07 | |||
|
|
6898e548a5 | ||
| 8c88e4e6a2 | |||
| 5ca4c5cc81 | |||
| eb0f2d521e | |||
| d68e423768 | |||
|
|
4ca7f9053f | ||
|
|
d61b90baa4 | ||
|
|
4a9c9ab1f3 | ||
|
|
6fb4fa7683 | ||
|
|
911eb60ae9 | ||
| c116efd6f4 | |||
|
|
8e3c62a518 | ||
|
|
16bf2c9494 | ||
| 9a9a4dad01 | |||
|
|
07b5f5c00b | ||
|
|
b60225cb74 | ||
|
|
250b2e9509 | ||
| f68c1c95eb | |||
|
|
e71a518b49 | ||
| 5531f6e87a | |||
|
|
a8ec29d2ed | ||
| 19aab99398 | |||
| 0b1ed48471 | |||
|
|
0b9c6320eb | ||
|
|
03b20787b3 | ||
|
|
9f8f411c33 | ||
|
|
76438459f7 | ||
| 92ea6026f8 | |||
|
|
9031ea6b3f | ||
|
|
b576440612 | ||
|
|
a4891131fc | ||
|
|
5f57ce54aa | ||
|
|
f1859a5877 | ||
| 94d038d563 | |||
| ffae1e583b | |||
| 8916cbd097 | |||
| 790a75ac87 | |||
| d4f8165b87 | |||
|
|
8948d0fb18 | ||
|
|
908da0bc47 | ||
|
|
aae17208b0 | ||
|
|
cbd8918c61 | ||
|
|
f2b4f9e8fc | ||
|
|
a2a11647bb | ||
|
|
1d9c275b61 | ||
|
|
52468928c6 | ||
|
|
3d9471779a | ||
|
|
155c4b00d5 | ||
|
|
f4b45e9eae | ||
|
|
d836b58b2d | ||
|
|
4fc747f1c6 | ||
|
|
b27f2c43ae | ||
|
|
c5f947e14a | ||
|
|
67c41ab3ee | ||
|
|
9f330e2245 | ||
|
|
d785262312 | ||
|
|
27faaabcf2 | ||
| 226d68cb1c | |||
|
|
e776cc2319 | ||
|
|
3cdb1e511d | ||
| a4f867665f | |||
|
|
6b0583b139 | ||
| 2aad0c65c2 | |||
| 6b646378b6 | |||
| 747ad6387b | |||
| 228e66315c | |||
|
|
97c283797a | ||
|
|
eb1fade6f5 | ||
|
|
8d6071f794 | ||
|
|
8a109e34f8 | ||
|
|
fd72d72692 | ||
|
|
63ffacff96 | ||
|
|
1b4bb6fccc | ||
|
|
9b492b5e0d | ||
|
|
8427bd9f6b | ||
|
|
2c915161d5 | ||
|
|
75b06ca770 | ||
|
|
c3468a3387 | ||
|
|
a2f4adb647 | ||
|
|
403f69df8b | ||
|
|
12cf10f97a | ||
|
|
6084befe2c | ||
|
|
1aa99ea613 | ||
|
|
d539c0f808 | ||
|
|
bc509806fb | ||
|
|
c52820550f | ||
|
|
98b30f90a1 | ||
|
|
4efbafc174 | ||
|
|
6d3fda50d3 | ||
|
|
70b936012f | ||
|
|
54917fbe6d | ||
|
|
abeb9f054d | ||
|
|
c6d6c5fb2a | ||
|
|
5b0d7f0012 | ||
|
|
d9043aab0a | ||
|
|
b9281b68ab | ||
|
|
5c6a20be4e | ||
|
|
1c0a65957d | ||
|
|
7c315624b1 | ||
|
|
0572caa528 | ||
|
|
4233040585 | ||
|
|
c27dc8e380 | ||
|
|
e746756e56 | ||
|
|
1829d1cd0b | ||
|
|
fb979e5639 | ||
|
|
e7d0a85ad5 | ||
|
|
a384711327 | ||
|
|
3fd4778a48 | ||
|
|
4841dc09b3 | ||
|
|
b3aa4fc776 | ||
|
|
a9b3b8b6f4 | ||
|
|
56ef196695 | ||
| 242238d341 | |||
| f66f6d38fe | |||
| d58077f58b | |||
| 4d4d6dbedf | |||
| f60b276916 | |||
| 87857fd499 | |||
| 3c371cd079 | |||
|
|
428b849bcc | ||
|
|
85f3b4f607 | ||
|
|
916396f855 | ||
|
|
211c8d2b04 | ||
|
|
92e274d3fd | ||
|
|
d511ea48d5 | ||
|
|
1aa4da1adf | ||
|
|
0e8b6b0b6b | ||
|
|
1a2c1b976f | ||
|
|
1cc242fa51 | ||
|
|
18dfdba15d | ||
|
|
b04ac4eec6 | ||
|
|
c009f0c891 | ||
|
|
d2dc0bd295 | ||
|
|
ddbb5b7f19 | ||
|
|
954c25090b | ||
|
|
0b6cc59de1 | ||
|
|
2271b5741d | ||
|
|
8a438b041f | ||
|
|
dd92fcc4d8 | ||
|
|
8f66ca0e16 | ||
|
|
895ba1d24a | ||
| e49b807bef | |||
|
|
73c15b5e93 | ||
|
|
e505ea8c51 | ||
|
|
21e7df7c3e | ||
|
|
2d72ca66a4 | ||
|
|
4725a30165 | ||
|
|
f3c977f1b3 | ||
|
|
9a0e7265c6 | ||
|
|
3f8e2fbe6b | ||
|
|
590b13e916 | ||
|
|
0f6aee56e5 | ||
|
|
daf18e7295 | ||
|
|
9bcc87f663 | ||
|
|
e7205ce0aa | ||
|
|
e3c4b2edc8 | ||
|
|
222a3b35a2 | ||
|
|
cd5dfd56b2 | ||
|
|
7d5c6b8222 | ||
|
|
4dbf4736e4 | ||
|
|
d50504181e | ||
|
|
c7e94dfcd1 | ||
|
|
a752b67ca1 | ||
|
|
078736337d | ||
|
|
de1058a28c | ||
|
|
740797a689 | ||
|
|
26328920a2 | ||
|
|
9c447bbdf9 | ||
|
|
fac85a889f | ||
|
|
f5d898c89e | ||
| 974a4b634a | |||
| 3127c83603 | |||
|
|
8d69e43f72 | ||
|
|
86df9e7a50 | ||
| 59ff9bf818 | |||
|
|
1641e32e3d | ||
|
|
a482087abd | ||
| bc5b15cec2 | |||
| 3787c25a77 | |||
|
|
0b06b499e4 | ||
|
|
04079dd57b | ||
|
|
34ac0c5ab3 | ||
| 0d904b229e | |||
| c0f887ff9d | |||
|
|
cf95075d01 | ||
|
|
d78a764d87 | ||
|
|
a368f4b722 | ||
|
|
803fe4568f | ||
|
|
1162d5dcc1 | ||
|
|
aa83058e39 | ||
|
|
061f205224 | ||
|
|
5d966f98df | ||
|
|
0037914db8 | ||
|
|
13d0115475 | ||
|
|
5bdb5ad2bf | ||
| a5d733c8df | |||
|
|
0b038e2997 | ||
|
|
5e46040db6 | ||
|
|
f2b04dd0f6 | ||
|
|
2177c1b40e | ||
|
|
d1f4cffe8f | ||
|
|
74ce441b90 | ||
|
|
5893aa2426 | ||
|
|
cb7e7bf9d4 | ||
|
|
fbfdc6aa12 | ||
|
|
e7b6743e10 | ||
|
|
ff4283e917 | ||
|
|
890886d62d | ||
|
|
fd75dda2b1 | ||
|
|
f22c1aeae3 | ||
|
|
d68d49a469 | ||
|
|
1900d4eaf5 | ||
|
|
02833209d5 | ||
| 2058c0218c | |||
|
|
8896e723eb | ||
|
|
edcc614833 | ||
|
|
23fe1ff0be | ||
|
|
19d1dc9f28 | ||
|
|
24b93cfcad | ||
|
|
d3298fac8a | ||
|
|
fba5395bf0 | ||
|
|
2c4508ee16 | ||
| d239443555 | |||
| e45ad08fab | |||
| ddf5d26c4b | |||
|
|
ce74dcf912 | ||
|
|
41412e1ef4 | ||
|
|
1395d48cd0 | ||
|
|
418c3d4742 | ||
|
|
17ec962a22 | ||
|
|
989ee73549 | ||
|
|
7e452e1253 | ||
|
|
5bdb5c8025 | ||
|
|
924a5fea0b | ||
|
|
b51a57a6ee | ||
|
|
4079188881 | ||
|
|
174163e305 | ||
|
|
0886439685 | ||
|
|
34bf5a4fe8 | ||
|
|
e6a97f2b17 | ||
|
|
fecff625a3 | ||
|
|
6f540036a0 | ||
|
|
86d72aec39 | ||
|
|
39876832f3 | ||
|
|
f3af6ddbbc | ||
|
|
ba7299e20c | ||
|
|
5db9d934b2 | ||
|
|
5c8eebf12c | ||
|
|
e725f6d2b2 | ||
|
|
494b655156 | ||
|
|
2940f2557c | ||
|
|
5e4660670f | ||
| e8d592ae76 | |||
|
|
97ea51df59 | ||
|
|
986061dc97 | ||
|
|
fe1910d16f | ||
|
|
63cb1aaa74 | ||
| 49ebd50077 | |||
|
|
4a6f874210 | ||
|
|
9394c7a9c5 | ||
|
|
7e502420fa | ||
|
|
12f4b764de | ||
|
|
4da4b7d552 | ||
|
|
d38abbbaa0 | ||
|
|
67bf7f649e | ||
|
|
acb35403b0 | ||
|
|
7d5dccc649 | ||
|
|
a7e0e7b217 | ||
|
|
9ce75b2dda | ||
|
|
d2022819f6 | ||
|
|
c8b342ba01 | ||
|
|
63823d5c89 | ||
|
|
cb17cc32da | ||
|
|
14d0e6d438 | ||
|
|
878fbad06a | ||
|
|
deb0506163 | ||
|
|
c4aeb673fd | ||
|
|
915ee59643 | ||
|
|
1568e120be | ||
|
|
d19dd3496d | ||
|
|
62c86ce477 | ||
|
|
c727eddc54 | ||
|
|
9c946ef6dc | ||
|
|
38a04fc4b2 | ||
| bded794647 | |||
|
|
539cb1de99 | ||
| 2e9ff47dbb | |||
|
|
c01079af1b | ||
|
|
cca1acb6f6 | ||
|
|
d7e502e22f | ||
|
|
bbeab360bc | ||
|
|
a78b7fdb29 | ||
| 273fbe2261 | |||
| ba9855c616 | |||
| c54f894f4f | |||
|
|
9f88f92ec0 | ||
|
|
a80e96c2cd | ||
|
|
7774612810 | ||
| 088ea1817c | |||
|
|
f362c8f7ef | ||
|
|
648f42b7e0 | ||
|
|
9a56cc350d | ||
|
|
50cd49217f | ||
|
|
7ed4b7db57 | ||
| b359cd623b | |||
|
|
a363e8dc34 | ||
|
|
52affc0d76 | ||
|
|
fe26f29f93 | ||
|
|
67b8725156 | ||
|
|
2a235b2bc9 | ||
|
|
dd022cf356 | ||
|
|
62e5bb30e2 | ||
|
|
675e11960a | ||
|
|
0c274ecbe0 | ||
|
|
2dfcd3f131 | ||
|
|
053acd138f | ||
|
|
3f20ae62be | ||
|
|
d342c7c827 | ||
|
|
3da0cfd0d0 | ||
|
|
acc4045580 | ||
|
|
6ee577302f | ||
|
|
d52856180a | ||
|
|
d4d479ca20 | ||
|
|
364af4b9c5 | ||
|
|
9e0d81fb1d | ||
|
|
2ee2c37479 | ||
|
|
528925b969 | ||
| 4851b40777 | |||
|
|
6372ad4e0a | ||
|
|
465bc9137e | ||
|
|
e8b6f5d893 | ||
|
|
54b697f2ee | ||
|
|
70df428825 | ||
|
|
8993d66056 | ||
|
|
863e6fb25e | ||
|
|
181856173e | ||
|
|
576fe59bbc | ||
|
|
c73aca71f7 | ||
|
|
ce264de963 | ||
|
|
1feb0cf83f | ||
|
|
6292624d41 | ||
| 4271a07f03 | |||
|
|
254fb6916f | ||
|
|
21857325a2 | ||
|
|
175d6860a3 | ||
|
|
d1c8f98408 | ||
|
|
3499fa9067 | ||
|
|
cca2cd774c | ||
|
|
6d60f8adb8 | ||
|
|
3b406a7974 | ||
| a116b3359c | |||
| 928019390b | |||
| 022b698f54 | |||
| 0228ac8393 | |||
|
|
a99f381f7f | ||
|
|
7c0af24bf5 | ||
|
|
d3aa45cfb9 | ||
|
|
f5461deb81 | ||
|
|
c19068128f | ||
|
|
1367daf1b7 | ||
|
|
5fc6e74cd6 | ||
|
|
5d7227c009 | ||
| 3a9c670172 | |||
|
|
2768faed53 | ||
|
|
85f3d6f09f |
162
.github/workflows/build-app.yml
vendored
162
.github/workflows/build-app.yml
vendored
@@ -1,162 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
name: Build AUTO_MAA
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pre_check:
|
||||
name: Pre Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repo Check
|
||||
id: repo_check
|
||||
run: |
|
||||
if [[ "$GITHUB_REPOSITORY" != "DLmaster361/AUTO_MAA" ]]; then
|
||||
echo "When forking this repository to make your own builds, you have to adjust this check."
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
build_AUTO_MAA:
|
||||
runs-on: windows-latest
|
||||
needs: pre_check
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
pip install -r requirements.txt
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Package
|
||||
id: package
|
||||
run: |
|
||||
copy app\utils\package.py .\
|
||||
python package.py
|
||||
- name: Read version
|
||||
id: read_version
|
||||
run: |
|
||||
$MAIN_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 1).Trim()
|
||||
"AUTO_MAA_version=$MAIN_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
$UPDATER_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 2 | Select-Object -Index 1).Trim()
|
||||
"updater_version=$UPDATER_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
- name: Create Zip
|
||||
id: create_zip
|
||||
run: |
|
||||
Compress-Archive -Path app,resources,main.py,AUTO_MAA.exe,requirements.txt,README.md,LICENSE -DestinationPath AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
|
||||
Compress-Archive -Path Updater.exe -DestinationPath Updater_${{ env.updater_version }}.zip
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AUTO_MAA_${{ env.AUTO_MAA_version }}
|
||||
path: |
|
||||
AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
|
||||
Updater_${{ env.updater_version }}.zip
|
||||
- name: Upload Version_Info Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: version_info
|
||||
path: version_info.txt
|
||||
publish_release:
|
||||
name: Publish release
|
||||
needs: build_AUTO_MAA
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: AUTO_MAA_*
|
||||
merge-multiple: true
|
||||
path: artifacts
|
||||
- name: Download Version_Info
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: version_info
|
||||
path: ./
|
||||
- name: Check if release exists
|
||||
id: check_if_release_exists
|
||||
run: |
|
||||
release_id=$(gh release view $(sed 's/\r$//g' <(head -n 1 version_info.txt)) --json id --jq .id || true)
|
||||
if [[ -z $release_id ]]; then
|
||||
echo "release_exists=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "release_exists=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
- name: Create release
|
||||
id: create_release
|
||||
if: steps.check_if_release_exists.outputs.release_exists == 'false'
|
||||
run: |
|
||||
set -xe
|
||||
shopt -s nullglob
|
||||
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
||||
TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
||||
NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
|
||||
NOTES_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
||||
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
|
||||
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" artifacts/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
- name: Update release
|
||||
id: update_release
|
||||
if: steps.check_if_release_exists.outputs.release_exists == 'true'
|
||||
run: |
|
||||
set -xe
|
||||
shopt -s nullglob
|
||||
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
||||
TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
||||
NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
|
||||
NOTES_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
||||
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
|
||||
gh release delete "$TAGNAME" --yes
|
||||
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" artifacts/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
- name: Setup SSH Key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
|
||||
- name: Upload Release to Server
|
||||
run: |
|
||||
scp -r artifacts/* ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/home/user/files/AUTO_MAA/
|
||||
162
.github/workflows/build-pre.yml
vendored
162
.github/workflows/build-pre.yml
vendored
@@ -1,162 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
name: Build AUTO_MAA_Pre
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "dev" ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pre_check:
|
||||
name: Pre Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repo Check
|
||||
id: repo_check
|
||||
run: |
|
||||
if [[ "$GITHUB_REPOSITORY" != "DLmaster361/AUTO_MAA" ]]; then
|
||||
echo "When forking this repository to make your own builds, you have to adjust this check."
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
build_AUTO_MAA:
|
||||
runs-on: windows-latest
|
||||
needs: pre_check
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
pip install -r requirements.txt
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Package
|
||||
id: package
|
||||
run: |
|
||||
copy app\utils\package.py .\
|
||||
python package.py
|
||||
- name: Read version
|
||||
id: read_version
|
||||
run: |
|
||||
$MAIN_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 1).Trim()
|
||||
"AUTO_MAA_version=$MAIN_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
$UPDATER_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 2 | Select-Object -Index 1).Trim()
|
||||
"updater_version=$UPDATER_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
- name: Create Zip
|
||||
id: create_zip
|
||||
run: |
|
||||
Compress-Archive -Path app,resources,main.py,AUTO_MAA.exe,requirements.txt,README.md,LICENSE -DestinationPath AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
|
||||
Compress-Archive -Path Updater.exe -DestinationPath Updater_${{ env.updater_version }}.zip
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AUTO_MAA_${{ env.AUTO_MAA_version }}
|
||||
path: |
|
||||
AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
|
||||
Updater_${{ env.updater_version }}.zip
|
||||
- name: Upload Version_Info Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: version_info
|
||||
path: version_info.txt
|
||||
publish_prerelease:
|
||||
name: Publish prerelease
|
||||
needs: build_AUTO_MAA
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: AUTO_MAA_*
|
||||
merge-multiple: true
|
||||
path: artifacts
|
||||
- name: Download Version_Info
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: version_info
|
||||
path: ./
|
||||
- name: Check if release exists
|
||||
id: check_if_release_exists
|
||||
run: |
|
||||
release_id=$(gh release view $(sed 's/\r$//g' <(head -n 1 version_info.txt)) --json id --jq .id || true)
|
||||
if [[ -z $release_id ]]; then
|
||||
echo "release_exists=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "release_exists=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
- name: Create prerelease
|
||||
id: create_prerelease
|
||||
if: steps.check_if_release_exists.outputs.release_exists == 'false'
|
||||
run: |
|
||||
set -xe
|
||||
shopt -s nullglob
|
||||
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
||||
TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
||||
NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
|
||||
NOTES_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
||||
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
|
||||
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" --prerelease artifacts/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
- name: Update prerelease
|
||||
id: update_prerelease
|
||||
if: steps.check_if_release_exists.outputs.release_exists == 'true'
|
||||
run: |
|
||||
set -xe
|
||||
shopt -s nullglob
|
||||
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
||||
TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
||||
NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
|
||||
NOTES_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
||||
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
|
||||
gh release delete "$TAGNAME" --yes
|
||||
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" --prerelease artifacts/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
- name: Setup SSH Key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
|
||||
- name: Upload Release to Server
|
||||
run: |
|
||||
scp -r artifacts/* ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/home/user/files/AUTO_MAA/
|
||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
logs/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
.python-version
|
||||
list/
|
||||
uv.lock
|
||||
|
||||
# IDE and editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# User files
|
||||
config/
|
||||
data/
|
||||
debug/
|
||||
history/
|
||||
script/
|
||||
res/notice.json
|
||||
res/theme_image.json
|
||||
res/images/Home/BannerTheme.jpg
|
||||
103
README.md
103
README.md
@@ -1,101 +1,2 @@
|
||||
# AUTO_MAA
|
||||
|
||||
MAA多账号管理与自动化软件
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
</h1>
|
||||
|
||||
[](https://github.com/DLmaster361/AUTO_MAA/stargazers)
|
||||
[](https://github.com/DLmaster361/AUTO_MAA/network)
|
||||
[](https://github.com/DLmaster361/AUTO_MAA/issues)
|
||||
[](https://github.com/DLmaster361/AUTO_MAA/graphs/contributors)
|
||||
[](https://github.com/DLmaster361/AUTO_MAA/blob/main/LICENSE)
|
||||
</div>
|
||||
|
||||
## 软件介绍
|
||||
|
||||
### 性质
|
||||
|
||||
本软件是明日方舟第三方软件`MAA`的第三方工具,即第3<sup>3</sup>方软件。旨在优化MAA多账号功能体验,并通过一些方法解决MAA项目未能解决的部分问题,提高代理的稳定性。
|
||||
|
||||
### 原理
|
||||
|
||||
本软件可以存储多个明日方舟账号数据,并通过以下流程实现代理功能:
|
||||
|
||||
1. **配置:** 根据对应用户的配置信息,生成配置文件并将其导入MAA。
|
||||
2. **监测:** 在MAA开始代理后,持续读取MAA的日志以判断其运行状态。当软件认定MAA出现异常时,通过重启MAA使之仍能继续完成任务。
|
||||
3. **循环:** 重复上述步骤,使MAA依次完成各个用户的自动代理任务。
|
||||
|
||||
### 优势
|
||||
|
||||
- **节省运行开销:** 只需要一份MAA软件与一个模拟器,无需多开就能完成多账号代理,羸弱的电脑也能代理日常。
|
||||
- **自定义空间大:** 依靠高级用户配置模式,支持MAA几乎所有设置选项自定义,支持模拟器多开。
|
||||
- **调度方法自由:** 通过调度队列功能,自由实现MAA多开等多种调度方式。
|
||||
- **一键代理无忧:** 无须中途手动修改MAA配置,将繁琐交给AUTO_MAA,把游戏留给自己。
|
||||
- **代理结果复核:** 通过人工排查功能核实各用户代理情况,堵住自动代理的最后一丝风险。
|
||||
|
||||
## 重要声明
|
||||
|
||||
本开发团队承诺,不会修改明日方舟游戏本体与相关配置文件。本项目使用GPL开源,相关细则如下:
|
||||
|
||||
- **作者:** AUTO_MAA软件作者为DLmaster、DLmaster361或DLmaster_361,以上均指代同一人。
|
||||
- **使用:** AUTO_MAA使用者可以按自己的意愿自由使用本软件。依据GPL,对于由此可能产生的损失,AUTO_MAA项目组不负任何责任。
|
||||
- **分发:** AUTO_MAA允许任何人自由分发本软件,包括进行商业活动牟利。若为直接分发本软件,必须遵循GPL向接收者提供本软件项目地址、完整的软件源码与GPL协议原文(件);若为修改软件后进行分发,必须遵循GPL向接收者提供本软件项目地址、修改前的完整软件源码副本与GPL协议原文(件),违反者可能会被追究法律责任。
|
||||
- **传播:** AUTO_MAA原则上允许传播者自由传播本软件,但无论在何种传播过程中,不得删除项目作者与开发者所留版权声明,不得隐瞒项目作者与相关开发者的存在。由于软件性质,项目组不希望发现任何人在明日方舟官方媒体(包括官方媒体账号与森空岛社区等)或明日方舟游戏相关内容(包括同好群、线下活动与游戏内容讨论等)下提及AUTO_MAA或MAA,希望各位理解。
|
||||
- **衍生:** AUTO_MAA允许任何人对软件本体或软件部分代码进行二次开发或利用。但依据GPL,相关成果再次分发时也必须使用GPL或兼容的协议开源。
|
||||
- **贡献:** 不论是直接参与软件的维护编写,或是撰写文档、测试、反馈BUG、给出建议、参与讨论,都为AUTO_MAA项目的发展完善做出了不可忽视的贡献。项目组提倡各位贡献者遵照GitHub开源社区惯例,发布Issues参与项目。避免私信或私发邮件(安全性漏洞或敏感问题除外),以帮助更多用户。
|
||||
|
||||
以上细则是本项目对GPL的相关补充与强调。未提及的以GPL为准,发生冲突的以本细则为准。如有不清楚的部分,请发Issues询问。若发生纠纷,相关内容也没有在Issues上提及的,项目组拥有最终解释权。
|
||||
|
||||
**注意**
|
||||
|
||||
- 由于本软件有修改其它目录JSON文件等行为,使用前请将AUTO_MAA添加入Windows Defender信任区以及防病毒软件的信任区或开发者目录,避免被误杀。
|
||||
|
||||
---
|
||||
|
||||
# 使用方法
|
||||
|
||||
访问AUTO_MAA官方文档站以获取使用指南和项目相关信息
|
||||
|
||||
- [AUTO_MAA官方文档站](https://clozya.github.io/AUTOMAA_docs)
|
||||
|
||||
---
|
||||
|
||||
# 关于
|
||||
|
||||
## 项目开发情况
|
||||
|
||||
可在[《AUTO_MAA开发者协作文档》](https://docs.qq.com/aio/DQ3Z5eHNxdmxFQmZX)的`开发任务`页面中查看开发进度。
|
||||
|
||||
## 贡献者
|
||||
|
||||
感谢以下贡献者对本项目做出的贡献
|
||||
|
||||
<a href="https://github.com/DLmaster361/AUTO_MAA/graphs/contributors">
|
||||
|
||||
<img src="https://contrib.rocks/image?repo=DLmaster361/AUTO_MAA" />
|
||||
|
||||
</a>
|
||||
|
||||

|
||||
|
||||
感谢 @ClozyA 为本项目提供的下载服务器
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#DLmaster361/AUTO_MAA&Date)
|
||||
|
||||
## 交流与赞助
|
||||
|
||||
欢迎加入AUTO_MAA项目组,欢迎反馈bug
|
||||
|
||||
- QQ交流群:[957750551](https://qm.qq.com/q/bd9fISNoME)
|
||||
|
||||
---
|
||||
|
||||
如果喜欢这个项目的话,给作者来杯咖啡吧!
|
||||
|
||||

|
||||
TEST
|
||||
TEST
|
||||
@@ -1,51 +1,34 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA主程序包
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
__version__ = "4.2.0"
|
||||
__version__ = "5.0.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .core import AppConfig, QueueConfig, MaaConfig, Task, TaskManager, MainTimer
|
||||
from .models import MaaManager
|
||||
from .services import Notify, Crypto, System
|
||||
from .ui import AUTO_MAA
|
||||
from .utils import Updater
|
||||
|
||||
__all__ = [
|
||||
"AppConfig",
|
||||
"QueueConfig",
|
||||
"MaaConfig",
|
||||
"Task",
|
||||
"TaskManager",
|
||||
"MainTimer",
|
||||
"MaaManager",
|
||||
"Notify",
|
||||
"Crypto",
|
||||
"System",
|
||||
"AUTO_MAA",
|
||||
"Updater",
|
||||
]
|
||||
from .api import *
|
||||
from .core import *
|
||||
from .models import *
|
||||
from .services import *
|
||||
from .utils import *
|
||||
|
||||
__all__ = ["api", "core", "models", "services", "utils"]
|
||||
|
||||
47
app/api/__init__.py
Normal file
47
app/api/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
__version__ = "5.0.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .core import router as core_router
|
||||
from .info import router as info_router
|
||||
from .scripts import router as scripts_router
|
||||
from .plan import router as plan_router
|
||||
from .queue import router as queue_router
|
||||
from .dispatch import router as dispatch_router
|
||||
from .history import router as history_router
|
||||
from .setting import router as setting_router
|
||||
from .update import router as update_router
|
||||
|
||||
__all__ = [
|
||||
"core_router",
|
||||
"info_router",
|
||||
"scripts_router",
|
||||
"plan_router",
|
||||
"queue_router",
|
||||
"dispatch_router",
|
||||
"history_router",
|
||||
"setting_router",
|
||||
"update_router",
|
||||
]
|
||||
95
app/api/core.py
Normal file
95
app/api/core.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import time
|
||||
import asyncio
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
|
||||
from app.core import Config, Broadcast, TaskManager
|
||||
from app.services import System
|
||||
from app.models.schema import *
|
||||
|
||||
router = APIRouter(prefix="/api/core", tags=["核心信息"])
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def connect_websocket(websocket: WebSocket):
|
||||
|
||||
if Config.websocket is not None:
|
||||
await websocket.close(code=1000, reason="已有连接")
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
Config.websocket = websocket
|
||||
last_pong = time.monotonic()
|
||||
last_ping = time.monotonic()
|
||||
data = {}
|
||||
|
||||
await TaskManager.start_startup_queue()
|
||||
|
||||
while True:
|
||||
|
||||
try:
|
||||
|
||||
data = await asyncio.wait_for(websocket.receive_json(), timeout=1000005.0)
|
||||
if data.get("type") == "Signal" and "Pong" in data.get("data", {}):
|
||||
last_pong = time.monotonic()
|
||||
elif data.get("type") == "Signal" and "Ping" in data.get("data", {}):
|
||||
await websocket.send_json(
|
||||
WebSocketMessage(
|
||||
id="Main", type="Signal", data={"Pong": "无描述"}
|
||||
).model_dump()
|
||||
)
|
||||
else:
|
||||
await Broadcast.put(data)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
if last_pong < last_ping:
|
||||
await websocket.close(code=1000, reason="Ping超时")
|
||||
break
|
||||
await websocket.send_json(
|
||||
WebSocketMessage(
|
||||
id="Main", type="Signal", data={"Ping": "无描述"}
|
||||
).model_dump()
|
||||
)
|
||||
last_ping = time.monotonic()
|
||||
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
|
||||
Config.websocket = None
|
||||
await System.set_power("KillSelf")
|
||||
|
||||
|
||||
@router.post("/close")
|
||||
async def close():
|
||||
"""关闭后端程序"""
|
||||
|
||||
try:
|
||||
await System.set_power("KillSelf")
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
87
app/api/dispatch.py
Normal file
87
app/api/dispatch.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from app.core import Config, TaskManager
|
||||
from app.services import System
|
||||
from app.models.schema import *
|
||||
|
||||
router = APIRouter(prefix="/api/dispatch", tags=["任务调度"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/start", summary="添加任务", response_model=TaskCreateOut, status_code=200
|
||||
)
|
||||
async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut:
|
||||
|
||||
try:
|
||||
task_id = await TaskManager.add_task(task.mode, task.taskId)
|
||||
except Exception as e:
|
||||
return TaskCreateOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
websocketId="",
|
||||
)
|
||||
return TaskCreateOut(websocketId=str(task_id))
|
||||
|
||||
|
||||
@router.post("/stop", summary="中止任务", response_model=OutBase, status_code=200)
|
||||
async def stop_task(task: DispatchIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await TaskManager.stop_task(task.taskId)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/set/power", summary="设置电源标志", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def set_power(task: PowerIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
Config.power_sign = task.signal
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/cancel/power", summary="取消电源任务", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def cancel_power_task() -> OutBase:
|
||||
|
||||
try:
|
||||
await System.cancel_power_task()
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
88
app/api/history.py
Normal file
88
app/api/history.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from app.core import Config
|
||||
from app.models.schema import *
|
||||
|
||||
router = APIRouter(prefix="/api/history", tags=["历史记录"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/search",
|
||||
summary="搜索历史记录总览信息",
|
||||
response_model=HistorySearchOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def search_history(history: HistorySearchIn) -> HistorySearchOut:
|
||||
|
||||
try:
|
||||
data = await Config.search_history(
|
||||
history.mode,
|
||||
datetime.strptime(history.start_date, "%Y-%m-%d").date(),
|
||||
datetime.strptime(history.end_date, "%Y-%m-%d").date(),
|
||||
)
|
||||
for date, users in data.items():
|
||||
for user, records in users.items():
|
||||
record = await Config.merge_statistic_info(records)
|
||||
# 安全检查:确保 index 字段存在
|
||||
if "index" not in record:
|
||||
record["index"] = []
|
||||
record["index"] = [HistoryIndexItem(**_) for _ in record["index"]]
|
||||
record = HistoryData(**record)
|
||||
data[date][user] = record
|
||||
except Exception as e:
|
||||
return HistorySearchOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
data={},
|
||||
)
|
||||
return HistorySearchOut(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/data",
|
||||
summary="从指定文件内获取历史记录数据",
|
||||
response_model=HistoryDataGetOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_history_data(history: HistoryDataGetIn = Body(...)) -> HistoryDataGetOut:
|
||||
|
||||
try:
|
||||
path = Path(history.jsonPath)
|
||||
data = await Config.merge_statistic_info([path])
|
||||
data.pop("index", None)
|
||||
data["log_content"] = path.with_suffix(".log").read_text(encoding="utf-8")
|
||||
data = HistoryData(**data)
|
||||
except Exception as e:
|
||||
return HistoryDataGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
data=HistoryData(**{}),
|
||||
)
|
||||
return HistoryDataGetOut(data=data)
|
||||
215
app/api/info.py
Normal file
215
app/api/info.py
Normal file
@@ -0,0 +1,215 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from app.core import Config
|
||||
from app.models.schema import *
|
||||
|
||||
router = APIRouter(prefix="/api/info", tags=["信息获取"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/version",
|
||||
summary="获取后端git版本信息",
|
||||
response_model=VersionOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_git_version() -> VersionOut:
|
||||
|
||||
try:
|
||||
is_latest, commit_hash, commit_time = await Config.get_git_version()
|
||||
except Exception as e:
|
||||
return VersionOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
if_need_update=False,
|
||||
current_hash="unknown",
|
||||
current_time="unknown",
|
||||
current_version=Config.version(),
|
||||
)
|
||||
return VersionOut(
|
||||
if_need_update=not is_latest,
|
||||
current_hash=commit_hash,
|
||||
current_time=commit_time,
|
||||
current_version=Config.version(),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/combox/stage",
|
||||
summary="获取关卡号下拉框信息",
|
||||
response_model=ComboBoxOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_stage_combox(
|
||||
stage: GetStageIn = Body(..., description="关卡号类型")
|
||||
) -> ComboBoxOut:
|
||||
|
||||
try:
|
||||
raw_data = await Config.get_stage_info(stage.type)
|
||||
data = (
|
||||
[ComboBoxItem(**item) for item in raw_data if isinstance(item, dict)]
|
||||
if raw_data
|
||||
else []
|
||||
)
|
||||
except Exception as e:
|
||||
return ComboBoxOut(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
|
||||
)
|
||||
return ComboBoxOut(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/combox/script",
|
||||
summary="获取脚本下拉框信息",
|
||||
response_model=ComboBoxOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_script_combox() -> ComboBoxOut:
|
||||
|
||||
try:
|
||||
raw_data = await Config.get_script_combox()
|
||||
data = [ComboBoxItem(**item) for item in raw_data] if raw_data else []
|
||||
except Exception as e:
|
||||
return ComboBoxOut(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
|
||||
)
|
||||
return ComboBoxOut(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/combox/task",
|
||||
summary="获取可选任务下拉框信息",
|
||||
response_model=ComboBoxOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_task_combox() -> ComboBoxOut:
|
||||
|
||||
try:
|
||||
raw_data = await Config.get_task_combox()
|
||||
data = [ComboBoxItem(**item) for item in raw_data] if raw_data else []
|
||||
except Exception as e:
|
||||
return ComboBoxOut(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
|
||||
)
|
||||
return ComboBoxOut(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/combox/plan",
|
||||
summary="获取可选计划下拉框信息",
|
||||
response_model=ComboBoxOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_plan_combox() -> ComboBoxOut:
|
||||
|
||||
try:
|
||||
raw_data = await Config.get_plan_combox()
|
||||
data = [ComboBoxItem(**item) for item in raw_data] if raw_data else []
|
||||
except Exception as e:
|
||||
return ComboBoxOut(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
|
||||
)
|
||||
return ComboBoxOut(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/notice/get", summary="获取通知信息", response_model=NoticeOut, status_code=200
|
||||
)
|
||||
async def get_notice_info() -> NoticeOut:
|
||||
|
||||
try:
|
||||
if_need_show, data = await Config.get_notice()
|
||||
except Exception as e:
|
||||
return NoticeOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
if_need_show=False,
|
||||
data={},
|
||||
)
|
||||
return NoticeOut(if_need_show=if_need_show, data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/notice/confirm", summary="确认通知", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def confirm_notice() -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.set("Data", "IfShowNotice", False)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
# @router.post(
|
||||
# "/apps_info", summary="获取可下载应用信息", response_model=InfoOut, status_code=200
|
||||
# )
|
||||
# async def get_apps_info() -> InfoOut:
|
||||
|
||||
# try:
|
||||
# data = await Config.get_server_info("apps_info")
|
||||
# except Exception as e:
|
||||
# return InfoOut(
|
||||
# code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data={}
|
||||
# )
|
||||
# return InfoOut(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webconfig",
|
||||
summary="获取配置分享中心的配置信息",
|
||||
response_model=InfoOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_web_config() -> InfoOut:
|
||||
|
||||
try:
|
||||
data = await Config.get_web_config()
|
||||
except Exception as e:
|
||||
return InfoOut(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data={}
|
||||
)
|
||||
return InfoOut(data={"WebConfig": data})
|
||||
|
||||
|
||||
@router.post(
|
||||
"/get/overview", summary="信息总览", response_model=InfoOut, status_code=200
|
||||
)
|
||||
async def get_overview() -> InfoOut:
|
||||
try:
|
||||
stage = await Config.get_stage_info("Info")
|
||||
proxy = await Config.get_proxy_overview()
|
||||
except Exception as e:
|
||||
return InfoOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
data={"Stage": [], "Proxy": []},
|
||||
)
|
||||
return InfoOut(data={"Stage": stage, "Proxy": proxy})
|
||||
106
app/api/plan.py
Normal file
106
app/api/plan.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from app.core import Config
|
||||
from app.models.schema import *
|
||||
|
||||
router = APIRouter(prefix="/api/plan", tags=["计划管理"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/add", summary="添加计划表", response_model=PlanCreateOut, status_code=200
|
||||
)
|
||||
async def add_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut:
|
||||
|
||||
try:
|
||||
uid, config = await Config.add_plan(plan.type)
|
||||
data = MaaPlanConfig(**(await config.toDict()))
|
||||
except Exception as e:
|
||||
return PlanCreateOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
planId="",
|
||||
data=MaaPlanConfig(**{}),
|
||||
)
|
||||
return PlanCreateOut(planId=str(uid), data=data)
|
||||
|
||||
|
||||
@router.post("/get", summary="查询计划表", response_model=PlanGetOut, status_code=200)
|
||||
async def get_plan(plan: PlanGetIn = Body(...)) -> PlanGetOut:
|
||||
|
||||
try:
|
||||
index, data = await Config.get_plan(plan.planId)
|
||||
index = [PlanIndexItem(**_) for _ in index]
|
||||
data = {uid: MaaPlanConfig(**cfg) for uid, cfg in data.items()}
|
||||
except Exception as e:
|
||||
return PlanGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
index=[],
|
||||
data={},
|
||||
)
|
||||
return PlanGetOut(index=index, data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/update", summary="更新计划表配置信息", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def update_plan(plan: PlanUpdateIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.update_plan(plan.planId, plan.data.model_dump(exclude_unset=True))
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post("/delete", summary="删除计划表", response_model=OutBase, status_code=200)
|
||||
async def delete_plan(plan: PlanDeleteIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.del_plan(plan.planId)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/order", summary="重新排序计划表", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def reorder_plan(plan: PlanReorderIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.reorder_plan(plan.indexList)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
259
app/api/queue.py
Normal file
259
app/api/queue.py
Normal file
@@ -0,0 +1,259 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from app.core import Config
|
||||
from app.models.schema import *
|
||||
|
||||
router = APIRouter(prefix="/api/queue", tags=["调度队列管理"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/add", summary="添加调度队列", response_model=QueueCreateOut, status_code=200
|
||||
)
|
||||
async def add_queue() -> QueueCreateOut:
|
||||
|
||||
try:
|
||||
uid, config = await Config.add_queue()
|
||||
data = QueueConfig(**(await config.toDict()))
|
||||
except Exception as e:
|
||||
return QueueCreateOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
queueId="",
|
||||
data=QueueConfig(**{}),
|
||||
)
|
||||
return QueueCreateOut(queueId=str(uid), data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/get", summary="查询调度队列配置信息", response_model=QueueGetOut, status_code=200
|
||||
)
|
||||
async def get_queues(queue: QueueGetIn = Body(...)) -> QueueGetOut:
|
||||
|
||||
try:
|
||||
index, config = await Config.get_queue(queue.queueId)
|
||||
index = [QueueIndexItem(**_) for _ in index]
|
||||
data = {uid: QueueConfig(**cfg) for uid, cfg in config.items()}
|
||||
except Exception as e:
|
||||
return QueueGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
index=[],
|
||||
data={},
|
||||
)
|
||||
return QueueGetOut(index=index, data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/update", summary="更新调度队列配置信息", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def update_queue(queue: QueueUpdateIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.update_queue(
|
||||
queue.queueId, queue.data.model_dump(exclude_unset=True)
|
||||
)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post("/delete", summary="删除调度队列", response_model=OutBase, status_code=200)
|
||||
async def delete_queue(queue: QueueDeleteIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.del_queue(queue.queueId)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post("/order", summary="重新排序", response_model=OutBase, status_code=200)
|
||||
async def reorder_queue(script: QueueReorderIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.reorder_queue(script.indexList)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/time/get", summary="查询定时项", response_model=TimeSetGetOut, status_code=200
|
||||
)
|
||||
async def get_time_set(time: TimeSetGetIn = Body(...)) -> TimeSetGetOut:
|
||||
|
||||
try:
|
||||
index, data = await Config.get_time_set(time.queueId, time.timeSetId)
|
||||
index = [TimeSetIndexItem(**_) for _ in index]
|
||||
data = {uid: TimeSet(**cfg) for uid, cfg in data.items()}
|
||||
except Exception as e:
|
||||
return TimeSetGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
index=[],
|
||||
data={},
|
||||
)
|
||||
return TimeSetGetOut(index=index, data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/time/add", summary="添加定时项", response_model=TimeSetCreateOut, status_code=200
|
||||
)
|
||||
async def add_time_set(time: QueueSetInBase = Body(...)) -> TimeSetCreateOut:
|
||||
|
||||
uid, config = await Config.add_time_set(time.queueId)
|
||||
data = TimeSet(**(await config.toDict()))
|
||||
return TimeSetCreateOut(timeSetId=str(uid), data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/time/update", summary="更新定时项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def update_time_set(time: TimeSetUpdateIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.update_time_set(
|
||||
time.queueId, time.timeSetId, time.data.model_dump(exclude_unset=True)
|
||||
)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/time/delete", summary="删除定时项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def delete_time_set(time: TimeSetDeleteIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.del_time_set(time.queueId, time.timeSetId)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/time/order", summary="重新排序定时项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def reorder_time_set(time: TimeSetReorderIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.reorder_time_set(time.queueId, time.indexList)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/item/get", summary="查询队列项", response_model=QueueItemGetOut, status_code=200
|
||||
)
|
||||
async def get_item(item: QueueItemGetIn = Body(...)) -> QueueItemGetOut:
|
||||
|
||||
try:
|
||||
index, data = await Config.get_queue_item(item.queueId, item.queueItemId)
|
||||
index = [QueueItemIndexItem(**_) for _ in index]
|
||||
data = {uid: QueueItem(**cfg) for uid, cfg in data.items()}
|
||||
except Exception as e:
|
||||
return QueueItemGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
index=[],
|
||||
data={},
|
||||
)
|
||||
return QueueItemGetOut(index=index, data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/item/add",
|
||||
summary="添加队列项",
|
||||
response_model=QueueItemCreateOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def add_item(item: QueueSetInBase = Body(...)) -> QueueItemCreateOut:
|
||||
|
||||
uid, config = await Config.add_queue_item(item.queueId)
|
||||
data = QueueItem(**(await config.toDict()))
|
||||
return QueueItemCreateOut(queueItemId=str(uid), data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/item/update", summary="更新队列项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def update_item(item: QueueItemUpdateIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.update_queue_item(
|
||||
item.queueId, item.queueItemId, item.data.model_dump(exclude_unset=True)
|
||||
)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/item/delete", summary="删除队列项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def delete_item(item: QueueItemDeleteIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.del_queue_item(item.queueId, item.queueItemId)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/item/order", summary="重新排序队列项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def reorder_item(item: QueueItemReorderIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.reorder_queue_item(item.queueId, item.indexList)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
283
app/api/scripts.py
Normal file
283
app/api/scripts.py
Normal file
@@ -0,0 +1,283 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import uuid
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from app.core import Config
|
||||
from app.models.schema import *
|
||||
|
||||
router = APIRouter(prefix="/api/scripts", tags=["脚本管理"])
|
||||
|
||||
|
||||
SCRIPT_BOOK = {"MaaConfig": MaaConfig, "GeneralConfig": GeneralConfig}
|
||||
USER_BOOK = {"MaaConfig": MaaUserConfig, "GeneralConfig": GeneralUserConfig}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/add", summary="添加脚本", response_model=ScriptCreateOut, status_code=200
|
||||
)
|
||||
async def add_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut:
|
||||
|
||||
try:
|
||||
uid, config = await Config.add_script(script.type)
|
||||
data = SCRIPT_BOOK[type(config).__name__](**(await config.toDict()))
|
||||
except Exception as e:
|
||||
return ScriptCreateOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
scriptId="",
|
||||
data=GeneralConfig(**{}),
|
||||
)
|
||||
return ScriptCreateOut(scriptId=str(uid), data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/get", summary="查询脚本配置信息", response_model=ScriptGetOut, status_code=200
|
||||
)
|
||||
async def get_script(script: ScriptGetIn = Body(...)) -> ScriptGetOut:
|
||||
|
||||
try:
|
||||
index, data = await Config.get_script(script.scriptId)
|
||||
index = [ScriptIndexItem(**_) for _ in index]
|
||||
data = {
|
||||
uid: SCRIPT_BOOK[next((_.type for _ in index if _.uid == uid), "General")](
|
||||
**cfg
|
||||
)
|
||||
for uid, cfg in data.items()
|
||||
}
|
||||
except Exception as e:
|
||||
return ScriptGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
index=[],
|
||||
data={},
|
||||
)
|
||||
return ScriptGetOut(index=index, data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/update", summary="更新脚本配置信息", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def update_script(script: ScriptUpdateIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.update_script(
|
||||
script.scriptId, script.data.model_dump(exclude_unset=True)
|
||||
)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post("/delete", summary="删除脚本", response_model=OutBase, status_code=200)
|
||||
async def delete_script(script: ScriptDeleteIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.del_script(script.scriptId)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post("/order", summary="重新排序脚本", response_model=OutBase, status_code=200)
|
||||
async def reorder_script(script: ScriptReorderIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.reorder_script(script.indexList)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/import/file", summary="从文件加载脚本", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def import_script_from_file(script: ScriptFileIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.import_script_from_file(script.scriptId, script.jsonFile)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/export/file", summary="导出脚本到文件", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def export_script_to_file(script: ScriptFileIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.export_script_to_file(script.scriptId, script.jsonFile)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/import/web", summary="从网络加载脚本", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def import_script_from_web(script: ScriptUrlIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.import_script_from_web(script.scriptId, script.url)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/Upload/web", summary="上传脚本配置到网络", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def upload_script_to_web(script: ScriptUploadIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.upload_script_to_web(
|
||||
script.scriptId, script.config_name, script.author, script.description
|
||||
)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/user/get", summary="查询用户", response_model=UserGetOut, status_code=200
|
||||
)
|
||||
async def get_user(user: UserGetIn = Body(...)) -> UserGetOut:
|
||||
|
||||
try:
|
||||
index, data = await Config.get_user(user.scriptId, user.userId)
|
||||
index = [UserIndexItem(**_) for _ in index]
|
||||
data = {
|
||||
uid: USER_BOOK[
|
||||
type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__
|
||||
](**cfg)
|
||||
for uid, cfg in data.items()
|
||||
}
|
||||
except Exception as e:
|
||||
return UserGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
index=[],
|
||||
data={},
|
||||
)
|
||||
return UserGetOut(index=index, data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/user/add", summary="添加用户", response_model=UserCreateOut, status_code=200
|
||||
)
|
||||
async def add_user(user: UserInBase = Body(...)) -> UserCreateOut:
|
||||
|
||||
try:
|
||||
uid, config = await Config.add_user(user.scriptId)
|
||||
data = USER_BOOK[type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__](
|
||||
**(await config.toDict())
|
||||
)
|
||||
except Exception as e:
|
||||
return UserCreateOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
userId="",
|
||||
data=GeneralUserConfig(**{}),
|
||||
)
|
||||
return UserCreateOut(userId=str(uid), data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/user/update", summary="更新用户配置信息", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def update_user(user: UserUpdateIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.update_user(
|
||||
user.scriptId, user.userId, user.data.model_dump(exclude_unset=True)
|
||||
)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/user/delete", summary="删除用户", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def delete_user(user: UserDeleteIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.del_user(user.scriptId, user.userId)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/user/order", summary="重新排序用户", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def reorder_user(user: UserReorderIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.reorder_user(user.scriptId, user.indexList)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/user/infrastructure",
|
||||
summary="导入基建配置文件",
|
||||
response_model=OutBase,
|
||||
status_code=200,
|
||||
)
|
||||
async def import_infrastructure(user: UserSetIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.set_infrastructure(user.scriptId, user.userId, user.jsonFile)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
268
app/api/setting.py
Normal file
268
app/api/setting.py
Normal file
@@ -0,0 +1,268 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from app.core import Config
|
||||
from app.services import System, Notify
|
||||
from app.models.schema import *
|
||||
import uuid
|
||||
|
||||
router = APIRouter(prefix="/api/setting", tags=["全局设置"])
|
||||
|
||||
|
||||
@router.post("/get", summary="查询配置", response_model=SettingGetOut, status_code=200)
|
||||
async def get_scripts() -> SettingGetOut:
|
||||
"""查询配置"""
|
||||
|
||||
try:
|
||||
data = await Config.get_setting()
|
||||
except Exception as e:
|
||||
return SettingGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
data=GlobalConfig(**{}),
|
||||
)
|
||||
return SettingGetOut(data=GlobalConfig(**data))
|
||||
|
||||
|
||||
@router.post("/update", summary="更新配置", response_model=OutBase, status_code=200)
|
||||
async def update_script(script: SettingUpdateIn = Body(...)) -> OutBase:
|
||||
"""更新配置"""
|
||||
|
||||
try:
|
||||
data = script.data.model_dump(exclude_unset=True)
|
||||
await Config.update_setting(data)
|
||||
|
||||
if data.get("Start", {}).get("IfSelfStart", None) is not None:
|
||||
await System.set_SelfStart()
|
||||
if data.get("Function", None) is not None:
|
||||
function = data["Function"]
|
||||
if function.get("IfAllowSleep", None) is not None:
|
||||
await System.set_Sleep()
|
||||
if function.get("IfSkipMumuSplashAds", None) is not None:
|
||||
MuMu_splash_ads_path = (
|
||||
Path(os.getenv("APPDATA") or "")
|
||||
/ "Netease/MuMuPlayer-12.0/data/startupImage"
|
||||
)
|
||||
if Config.get("Function", "IfSkipMumuSplashAds"):
|
||||
if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_dir():
|
||||
shutil.rmtree(MuMu_splash_ads_path)
|
||||
MuMu_splash_ads_path.touch()
|
||||
else:
|
||||
if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_file():
|
||||
MuMu_splash_ads_path.unlink()
|
||||
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/test_notify", summary="测试通知", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def test_notify() -> OutBase:
|
||||
"""测试通知"""
|
||||
|
||||
try:
|
||||
await Notify.send_test_notification()
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/create",
|
||||
summary="创建自定义Webhook",
|
||||
response_model=OutBase,
|
||||
status_code=200,
|
||||
)
|
||||
async def create_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||
"""创建自定义Webhook"""
|
||||
|
||||
try:
|
||||
# 生成唯一ID
|
||||
webhook_id = str(uuid.uuid4())
|
||||
|
||||
# 创建webhook配置
|
||||
webhook_config = {
|
||||
"id": webhook_id,
|
||||
"name": webhook_data.get("name", ""),
|
||||
"url": webhook_data.get("url", ""),
|
||||
"template": webhook_data.get("template", ""),
|
||||
"enabled": webhook_data.get("enabled", True),
|
||||
"headers": webhook_data.get("headers", {}),
|
||||
"method": webhook_data.get("method", "POST"),
|
||||
}
|
||||
|
||||
# 获取当前配置
|
||||
current_config = await Config.get_setting()
|
||||
custom_webhooks = current_config.get("Notify", {}).get("CustomWebhooks", [])
|
||||
|
||||
# 添加新webhook
|
||||
custom_webhooks.append(webhook_config)
|
||||
|
||||
# 更新配置
|
||||
update_data = {"Notify": {"CustomWebhooks": custom_webhooks}}
|
||||
await Config.update_setting(update_data)
|
||||
|
||||
return OutBase(message=f"Webhook '{webhook_config['name']}' 创建成功")
|
||||
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/update",
|
||||
summary="更新自定义Webhook",
|
||||
response_model=OutBase,
|
||||
status_code=200,
|
||||
)
|
||||
async def update_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||
"""更新自定义Webhook"""
|
||||
|
||||
try:
|
||||
webhook_id = webhook_data.get("id")
|
||||
if not webhook_id:
|
||||
return OutBase(code=400, status="error", message="缺少Webhook ID")
|
||||
|
||||
# 获取当前配置
|
||||
current_config = await Config.get_setting()
|
||||
custom_webhooks = current_config.get("Notify", {}).get("CustomWebhooks", [])
|
||||
|
||||
# 查找并更新webhook
|
||||
updated = False
|
||||
for i, webhook in enumerate(custom_webhooks):
|
||||
if webhook.get("id") == webhook_id:
|
||||
custom_webhooks[i].update(
|
||||
{
|
||||
"name": webhook_data.get("name", webhook.get("name", "")),
|
||||
"url": webhook_data.get("url", webhook.get("url", "")),
|
||||
"template": webhook_data.get(
|
||||
"template", webhook.get("template", "")
|
||||
),
|
||||
"enabled": webhook_data.get(
|
||||
"enabled", webhook.get("enabled", True)
|
||||
),
|
||||
"headers": webhook_data.get(
|
||||
"headers", webhook.get("headers", {})
|
||||
),
|
||||
"method": webhook_data.get(
|
||||
"method", webhook.get("method", "POST")
|
||||
),
|
||||
}
|
||||
)
|
||||
updated = True
|
||||
break
|
||||
|
||||
if not updated:
|
||||
return OutBase(code=404, status="error", message="Webhook不存在")
|
||||
|
||||
# 更新配置
|
||||
update_data = {"Notify": {"CustomWebhooks": custom_webhooks}}
|
||||
await Config.update_setting(update_data)
|
||||
|
||||
return OutBase(message="Webhook更新成功")
|
||||
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/delete",
|
||||
summary="删除自定义Webhook",
|
||||
response_model=OutBase,
|
||||
status_code=200,
|
||||
)
|
||||
async def delete_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||
"""删除自定义Webhook"""
|
||||
|
||||
try:
|
||||
webhook_id = webhook_data.get("id")
|
||||
if not webhook_id:
|
||||
return OutBase(code=400, status="error", message="缺少Webhook ID")
|
||||
|
||||
# 获取当前配置
|
||||
current_config = await Config.get_setting()
|
||||
custom_webhooks = current_config.get("Notify", {}).get("CustomWebhooks", [])
|
||||
|
||||
# 查找并删除webhook
|
||||
original_length = len(custom_webhooks)
|
||||
custom_webhooks = [w for w in custom_webhooks if w.get("id") != webhook_id]
|
||||
|
||||
if len(custom_webhooks) == original_length:
|
||||
return OutBase(code=404, status="error", message="Webhook不存在")
|
||||
|
||||
# 更新配置
|
||||
update_data = {"Notify": {"CustomWebhooks": custom_webhooks}}
|
||||
await Config.update_setting(update_data)
|
||||
|
||||
return OutBase(message="Webhook删除成功")
|
||||
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/test",
|
||||
summary="测试自定义Webhook",
|
||||
response_model=OutBase,
|
||||
status_code=200,
|
||||
)
|
||||
async def test_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||
"""测试自定义Webhook"""
|
||||
|
||||
try:
|
||||
webhook_config = {
|
||||
"name": webhook_data.get("name", "测试Webhook"),
|
||||
"url": webhook_data.get("url", ""),
|
||||
"template": webhook_data.get("template", ""),
|
||||
"enabled": True,
|
||||
"headers": webhook_data.get("headers", {}),
|
||||
"method": webhook_data.get("method", "POST"),
|
||||
}
|
||||
|
||||
await Notify.CustomWebhookPush(
|
||||
"AUTO-MAS Webhook测试",
|
||||
"这是一条测试消息,如果您收到此消息,说明Webhook配置正确!",
|
||||
webhook_config,
|
||||
)
|
||||
|
||||
return OutBase(message="Webhook测试成功")
|
||||
|
||||
except Exception as e:
|
||||
return OutBase(code=500, status="error", message=f"Webhook测试失败: {str(e)}")
|
||||
82
app/api/update.py
Normal file
82
app/api/update.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import asyncio
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
from app.core import Config
|
||||
from app.services import Updater
|
||||
from app.models.schema import *
|
||||
|
||||
router = APIRouter(prefix="/api/update", tags=["软件更新"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/check", summary="检查更新", response_model=UpdateCheckOut, status_code=200
|
||||
)
|
||||
async def check_update(version: UpdateCheckIn = Body(...)) -> UpdateCheckOut:
|
||||
|
||||
try:
|
||||
if_need, latest_version, update_info = await Updater.check_update(
|
||||
current_version=version.current_version, if_force=version.if_force
|
||||
)
|
||||
except Exception as e:
|
||||
return UpdateCheckOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
if_need_update=False,
|
||||
latest_version="",
|
||||
update_info={},
|
||||
)
|
||||
return UpdateCheckOut(
|
||||
if_need_update=if_need, latest_version=latest_version, update_info=update_info
|
||||
)
|
||||
|
||||
|
||||
@router.post("/download", summary="下载更新", response_model=OutBase, status_code=200)
|
||||
async def download_update() -> OutBase:
|
||||
|
||||
try:
|
||||
task = asyncio.create_task(Updater.download_update())
|
||||
Config.temp_task.append(task)
|
||||
task.add_done_callback(lambda t: Config.temp_task.remove(t))
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post("/install", summary="安装更新", response_model=OutBase, status_code=200)
|
||||
async def install_update() -> OutBase:
|
||||
|
||||
try:
|
||||
task = asyncio.create_task(Updater.install_update())
|
||||
Config.temp_task.append(task)
|
||||
task.add_done_callback(lambda t: Config.temp_task.remove(t))
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
@@ -1,46 +1,41 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA核心组件包
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
__version__ = "4.2.0"
|
||||
__version__ = "5.0.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .config import AppConfig, QueueConfig, MaaConfig, Config
|
||||
from .main_info_bar import MainInfoBar
|
||||
from .task_manager import Task, TaskManager
|
||||
from .broadcast import Broadcast
|
||||
from .config import Config, MaaConfig, GeneralConfig, MaaUserConfig, GeneralUserConfig
|
||||
from .timer import MainTimer
|
||||
from .task_manager import TaskManager
|
||||
|
||||
__all__ = [
|
||||
"AppConfig",
|
||||
"Broadcast",
|
||||
"Config",
|
||||
"QueueConfig",
|
||||
"MaaConfig",
|
||||
"MainInfoBar",
|
||||
"Task",
|
||||
"TaskManager",
|
||||
"GeneralConfig",
|
||||
"MainTimer",
|
||||
"TaskManager",
|
||||
"MaaUserConfig",
|
||||
"GeneralUserConfig",
|
||||
]
|
||||
|
||||
53
app/core/broadcast.py
Normal file
53
app/core/broadcast.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
from typing import Set
|
||||
|
||||
from app.utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger("消息广播")
|
||||
|
||||
|
||||
class _Broadcast:
|
||||
|
||||
def __init__(self):
|
||||
self.__subscribers: Set[asyncio.Queue] = set()
|
||||
|
||||
async def subscribe(self, queue: asyncio.Queue):
|
||||
"""订阅者注册"""
|
||||
self.__subscribers.add(queue)
|
||||
|
||||
async def unsubscribe(self, queue: asyncio.Queue):
|
||||
"""取消订阅"""
|
||||
self.__subscribers.remove(queue)
|
||||
|
||||
async def put(self, item):
|
||||
"""向所有订阅者广播消息"""
|
||||
logger.debug(f"向所有订阅者广播消息: {item}")
|
||||
for subscriber in self.__subscribers:
|
||||
await subscriber.put(deepcopy(item))
|
||||
|
||||
|
||||
Broadcast = _Broadcast()
|
||||
2826
app/core/config.py
2826
app/core/config.py
File diff suppressed because it is too large
Load Diff
@@ -1,92 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA信息通知栏
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtCore import Qt
|
||||
from qfluentwidgets import (
|
||||
InfoBar,
|
||||
InfoBarPosition,
|
||||
)
|
||||
|
||||
|
||||
class _MainInfoBar:
|
||||
"""信息通知栏"""
|
||||
|
||||
def __init__(self, main_window=None):
|
||||
|
||||
self.main_window = main_window
|
||||
|
||||
def push_info_bar(self, mode: str, title: str, content: str, time: int):
|
||||
"""推送到信息通知栏"""
|
||||
|
||||
if self.main_window is None:
|
||||
logger.error("信息通知栏未设置父窗口")
|
||||
return None
|
||||
|
||||
if mode == "success":
|
||||
InfoBar.success(
|
||||
title=title,
|
||||
content=content,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=time,
|
||||
parent=self.main_window,
|
||||
)
|
||||
elif mode == "warning":
|
||||
InfoBar.warning(
|
||||
title=title,
|
||||
content=content,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=time,
|
||||
parent=self.main_window,
|
||||
)
|
||||
elif mode == "error":
|
||||
InfoBar.error(
|
||||
title=title,
|
||||
content=content,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=time,
|
||||
parent=self.main_window,
|
||||
)
|
||||
elif mode == "info":
|
||||
InfoBar.info(
|
||||
title=title,
|
||||
content=content,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=time,
|
||||
parent=self.main_window,
|
||||
)
|
||||
|
||||
|
||||
MainInfoBar = _MainInfoBar()
|
||||
@@ -1,296 +1,395 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA业务调度器
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtCore import QThread, QObject, Signal
|
||||
from qfluentwidgets import Dialog
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Union
|
||||
import uuid
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing import Dict, Optional, Literal
|
||||
|
||||
from .config import Config
|
||||
from .main_info_bar import MainInfoBar
|
||||
from app.models import MaaManager
|
||||
from .config import Config, MaaConfig, GeneralConfig, QueueConfig
|
||||
from app.services import System
|
||||
from app.models.schema import WebSocketMessage
|
||||
from app.utils import get_logger
|
||||
from app.task import *
|
||||
from app.utils.constants import POWER_SIGN_MAP
|
||||
|
||||
|
||||
class Task(QThread):
|
||||
"""业务线程"""
|
||||
logger = get_logger("业务调度")
|
||||
|
||||
push_info_bar = Signal(str, str, str, int)
|
||||
question = Signal(str, str)
|
||||
question_response = Signal(bool)
|
||||
update_user_info = Signal(Path, list, list, list, list, list, list)
|
||||
create_task_list = Signal(list)
|
||||
create_user_list = Signal(list)
|
||||
update_task_list = Signal(list)
|
||||
update_user_list = Signal(list)
|
||||
update_log_text = Signal(str)
|
||||
accomplish = Signal(list)
|
||||
|
||||
def __init__(
|
||||
self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
|
||||
class _TaskManager:
|
||||
"""业务调度器"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.task_dict: Dict[uuid.UUID, asyncio.Task] = {}
|
||||
|
||||
async def add_task(
|
||||
self, mode: Literal["自动代理", "人工排查", "设置脚本"], uid: str
|
||||
) -> uuid.UUID:
|
||||
"""
|
||||
添加任务
|
||||
|
||||
:param mode: 任务模式
|
||||
:param uid: 任务UID
|
||||
"""
|
||||
|
||||
actual_id = uuid.UUID(uid)
|
||||
|
||||
if mode == "设置脚本":
|
||||
if actual_id in Config.ScriptConfig:
|
||||
task_id = actual_id
|
||||
actual_id = None
|
||||
else:
|
||||
for script_id, script in Config.ScriptConfig.items():
|
||||
if (
|
||||
isinstance(script, (MaaConfig | GeneralConfig))
|
||||
and actual_id in script.UserData
|
||||
):
|
||||
task_id = script_id
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"任务 {uid} 无法找到对应脚本配置")
|
||||
elif actual_id in Config.QueueConfig:
|
||||
task_id = actual_id
|
||||
actual_id = None
|
||||
elif actual_id in Config.ScriptConfig:
|
||||
task_id = uuid.uuid4()
|
||||
else:
|
||||
raise ValueError(f"任务 {uid} 无法找到对应脚本配置")
|
||||
|
||||
if task_id in self.task_dict or (
|
||||
actual_id is not None and actual_id in self.task_dict
|
||||
):
|
||||
raise RuntimeError(f"任务 {task_id} 已在运行")
|
||||
|
||||
logger.info(f"创建任务: {task_id}, 模式: {mode}")
|
||||
self.task_dict[task_id] = asyncio.create_task(
|
||||
self.run_task(mode, task_id, actual_id)
|
||||
)
|
||||
self.task_dict[task_id].add_done_callback(
|
||||
lambda t: asyncio.create_task(self.remove_task(t, mode, task_id))
|
||||
)
|
||||
|
||||
return task_id
|
||||
|
||||
@logger.catch
|
||||
async def run_task(
|
||||
self, mode: str, task_id: uuid.UUID, actual_id: Optional[uuid.UUID]
|
||||
):
|
||||
super(Task, self).__init__()
|
||||
|
||||
self.mode = mode
|
||||
self.name = name
|
||||
self.info = info
|
||||
logger.info(f"开始运行任务: {task_id}, 模式: {mode}")
|
||||
|
||||
self.logs = []
|
||||
if mode == "设置脚本":
|
||||
|
||||
self.question_response.connect(lambda: print("response"))
|
||||
if isinstance(Config.ScriptConfig[task_id], MaaConfig):
|
||||
task_item = MaaManager(mode, task_id, actual_id, str(task_id))
|
||||
elif isinstance(Config.ScriptConfig[task_id], GeneralConfig):
|
||||
task_item = GeneralManager(mode, task_id, actual_id, str(task_id))
|
||||
else:
|
||||
logger.error(
|
||||
f"不支持的脚本类型: {type(Config.ScriptConfig[task_id]).__name__}"
|
||||
)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id),
|
||||
type="Info",
|
||||
data={"Error": "脚本类型不支持"},
|
||||
).model_dump()
|
||||
)
|
||||
return
|
||||
|
||||
def run(self):
|
||||
|
||||
if "设置MAA" in self.mode:
|
||||
|
||||
logger.info(f"任务开始:设置{self.name}")
|
||||
self.push_info_bar.emit("info", "设置MAA", self.name, 3000)
|
||||
|
||||
self.task = MaaManager(
|
||||
self.mode,
|
||||
Config.app_path / f"config/MaaConfig/{self.name}",
|
||||
(
|
||||
None
|
||||
if "全局" in self.mode
|
||||
else Config.app_path
|
||||
/ f"config/MaaConfig/{self.name}/beta/{self.info["SetMaaInfo"]["UserId"]}/{self.info["SetMaaInfo"]["SetType"]}"
|
||||
),
|
||||
uid = actual_id or uuid.uuid4()
|
||||
self.task_dict[uid] = asyncio.create_task(task_item.run())
|
||||
self.task_dict[uid].add_done_callback(
|
||||
lambda t: asyncio.create_task(task_item.final_task(t))
|
||||
)
|
||||
self.task.push_info_bar.connect(self.push_info_bar.emit)
|
||||
self.task.accomplish.connect(lambda: self.accomplish.emit([]))
|
||||
|
||||
self.task.run()
|
||||
self.task_dict[uid].add_done_callback(partial(self.task_dict.pop, uid))
|
||||
try:
|
||||
await self.task_dict[uid]
|
||||
except Exception as e:
|
||||
logger.error(f"任务 {task_id} 运行出错: {type(e).__name__}: {str(e)}")
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id),
|
||||
type="Info",
|
||||
data={"Error": f"任务运行时出错 {type(e).__name__}: {str(e)}"},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
self.member_dict = self.search_member()
|
||||
self.task_dict = [
|
||||
[value, "等待"]
|
||||
for _, value in self.info["Queue"].items()
|
||||
if value != "禁用"
|
||||
]
|
||||
# 初始化任务列表
|
||||
if task_id in Config.QueueConfig:
|
||||
|
||||
self.create_task_list.emit(self.task_dict)
|
||||
queue = Config.QueueConfig[task_id]
|
||||
if not isinstance(queue, QueueConfig):
|
||||
return
|
||||
|
||||
for i in range(len(self.task_dict)):
|
||||
task_list = []
|
||||
for queue_item in queue.QueueItem.values():
|
||||
if queue_item.get("Info", "ScriptId") == "-":
|
||||
continue
|
||||
script_id = uuid.UUID(queue_item.get("Info", "ScriptId"))
|
||||
script = Config.ScriptConfig[script_id]
|
||||
if not isinstance(script, (MaaConfig | GeneralConfig)):
|
||||
logger.error(f"不支持的脚本类型: {type(script).__name__}")
|
||||
continue
|
||||
task_list.append(
|
||||
{
|
||||
"script_id": str(script_id),
|
||||
"status": "等待",
|
||||
"name": script.get("Info", "Name"),
|
||||
"user_list": [
|
||||
{
|
||||
"user_id": str(user_id),
|
||||
"status": "等待",
|
||||
"name": config.get("Info", "Name"),
|
||||
}
|
||||
for user_id, config in script.UserData.items()
|
||||
if config.get("Info", "Status")
|
||||
and config.get("Info", "RemainedDay") != 0
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
elif actual_id is not None and actual_id in Config.ScriptConfig:
|
||||
|
||||
self.task_dict[i][1] = "运行"
|
||||
self.update_task_list.emit(self.task_dict)
|
||||
script = Config.ScriptConfig[actual_id]
|
||||
if not isinstance(script, (MaaConfig | GeneralConfig)):
|
||||
logger.error(f"不支持的脚本类型: {type(script).__name__}")
|
||||
return
|
||||
|
||||
if self.task_dict[i][0] in Config.running_list:
|
||||
task_list = [
|
||||
{
|
||||
"script_id": str(actual_id),
|
||||
"status": "等待",
|
||||
"name": script.get("Info", "Name"),
|
||||
"user_list": [
|
||||
{
|
||||
"user_id": str(user_id),
|
||||
"status": "等待",
|
||||
"name": config.get("Info", "Name"),
|
||||
}
|
||||
for user_id, config in script.UserData.items()
|
||||
if config.get("Info", "Status")
|
||||
and config.get("Info", "RemainedDay") != 0
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
self.task_dict[i][1] = "跳过"
|
||||
self.update_task_list.emit(self.task_dict)
|
||||
logger.info(f"跳过任务:{self.task_dict[i][0]}")
|
||||
self.push_info_bar.emit(
|
||||
"info", "跳过任务", self.task_dict[i][0], 3000
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id), type="Update", data={"task_dict": task_list}
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
# 清理用户列表初值
|
||||
for task in task_list:
|
||||
task.pop("user_list", None)
|
||||
|
||||
for task in task_list:
|
||||
|
||||
script_id = uuid.UUID(task["script_id"])
|
||||
|
||||
# 检查任务是否在运行列表中
|
||||
if script_id in self.task_dict:
|
||||
|
||||
task["status"] = "跳过"
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id),
|
||||
type="Update",
|
||||
data={"task_list": task_list},
|
||||
).model_dump()
|
||||
)
|
||||
logger.info(f"跳过任务: {script_id}, 该任务已在运行列表中")
|
||||
continue
|
||||
|
||||
# 检查任务对应脚本是否仍存在
|
||||
if script_id in self.task_dict:
|
||||
|
||||
task["status"] = "异常"
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id),
|
||||
type="Update",
|
||||
data={"task_list": task_list},
|
||||
).model_dump()
|
||||
)
|
||||
logger.info(f"跳过任务: {script_id}, 该任务对应脚本已被删除")
|
||||
continue
|
||||
|
||||
# 标记为运行中
|
||||
task["status"] = "运行"
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id), type="Update", data={"task_list": task_list}
|
||||
).model_dump()
|
||||
)
|
||||
logger.info(f"任务开始: {script_id}")
|
||||
|
||||
if isinstance(Config.ScriptConfig[script_id], MaaConfig):
|
||||
task_item = MaaManager(mode, script_id, None, str(task_id))
|
||||
elif isinstance(Config.ScriptConfig[script_id], GeneralConfig):
|
||||
task_item = GeneralManager(mode, script_id, actual_id, str(task_id))
|
||||
else:
|
||||
logger.error(
|
||||
f"不支持的脚本类型: {type(Config.ScriptConfig[script_id]).__name__}"
|
||||
)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id),
|
||||
type="Info",
|
||||
data={"Error": "脚本类型不支持"},
|
||||
).model_dump()
|
||||
)
|
||||
continue
|
||||
|
||||
Config.running_list.append(self.task_dict[i][0])
|
||||
logger.info(f"任务开始:{self.task_dict[i][0]}")
|
||||
self.push_info_bar.emit("info", "任务开始", self.task_dict[i][0], 3000)
|
||||
|
||||
if self.member_dict[self.task_dict[i][0]][0] == "Maa":
|
||||
|
||||
self.task = MaaManager(
|
||||
self.mode[0:4],
|
||||
self.member_dict[self.task_dict[i][0]][1],
|
||||
)
|
||||
|
||||
self.task.question.connect(self.question.emit)
|
||||
self.question_response.disconnect()
|
||||
self.question_response.connect(self.task.question_response.emit)
|
||||
self.task.push_info_bar.connect(self.push_info_bar.emit)
|
||||
self.task.create_user_list.connect(self.create_user_list.emit)
|
||||
self.task.update_user_list.connect(self.update_user_list.emit)
|
||||
self.task.update_log_text.connect(self.update_log_text.emit)
|
||||
self.task.update_user_info.connect(
|
||||
lambda modes, uids, days, lasts, notes, numbs: self.update_user_info.emit(
|
||||
self.member_dict[self.task_dict[i][0]][1],
|
||||
modes,
|
||||
uids,
|
||||
days,
|
||||
lasts,
|
||||
notes,
|
||||
numbs,
|
||||
)
|
||||
)
|
||||
self.task.accomplish.connect(
|
||||
lambda log: self.task_accomplish(self.task_dict[i][0], log)
|
||||
)
|
||||
|
||||
self.task.run()
|
||||
|
||||
Config.running_list.remove(self.task_dict[i][0])
|
||||
|
||||
self.task_dict[i][1] = "完成"
|
||||
logger.info(f"任务完成:{self.task_dict[i][0]}")
|
||||
self.push_info_bar.emit("info", "任务完成", self.task_dict[i][0], 3000)
|
||||
|
||||
self.accomplish.emit(self.logs)
|
||||
|
||||
def search_member(self) -> dict:
|
||||
"""搜索所有脚本实例及其路径"""
|
||||
|
||||
member_dict = {}
|
||||
|
||||
if (Config.app_path / "config/MaaConfig").exists():
|
||||
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
|
||||
if subdir.is_dir():
|
||||
|
||||
member_dict[subdir.name] = ["Maa", subdir]
|
||||
|
||||
return member_dict
|
||||
|
||||
def task_accomplish(self, name: str, log: dict):
|
||||
"""保存保存任务结果"""
|
||||
|
||||
self.logs.append([name, log])
|
||||
self.task.deleteLater()
|
||||
|
||||
|
||||
class _TaskManager(QObject):
|
||||
"""业务调度器"""
|
||||
|
||||
create_gui = Signal(Task)
|
||||
connect_gui = Signal(Task)
|
||||
push_info_bar = Signal(str, str, str, int)
|
||||
|
||||
def __init__(self):
|
||||
super(_TaskManager, self).__init__()
|
||||
|
||||
self.task_dict: Dict[str, Task] = {}
|
||||
|
||||
def add_task(
|
||||
self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
|
||||
):
|
||||
"""添加任务"""
|
||||
|
||||
if name in Config.running_list or name in self.task_dict:
|
||||
|
||||
logger.warning(f"任务已存在:{name}")
|
||||
MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
|
||||
return None
|
||||
|
||||
logger.info(f"任务开始:{name}")
|
||||
MainInfoBar.push_info_bar("info", "任务开始", name, 3000)
|
||||
|
||||
Config.running_list.append(name)
|
||||
self.task_dict[name] = Task(mode, name, info)
|
||||
self.task_dict[name].question.connect(
|
||||
lambda title, content: self.push_dialog(name, title, content)
|
||||
)
|
||||
self.task_dict[name].push_info_bar.connect(MainInfoBar.push_info_bar)
|
||||
self.task_dict[name].update_user_info.connect(Config.change_user_info)
|
||||
self.task_dict[name].accomplish.connect(
|
||||
lambda logs: self.remove_task(mode, name, logs)
|
||||
)
|
||||
|
||||
if "新调度台" in mode:
|
||||
self.create_gui.emit(self.task_dict[name])
|
||||
|
||||
elif "主调度台" in mode:
|
||||
self.connect_gui.emit(self.task_dict[name])
|
||||
|
||||
self.task_dict[name].start()
|
||||
|
||||
def stop_task(self, name: str):
|
||||
"""中止任务"""
|
||||
|
||||
logger.info(f"中止任务:{name}")
|
||||
MainInfoBar.push_info_bar("info", "中止任务", name, 3000)
|
||||
|
||||
if name == "ALL":
|
||||
|
||||
for name in self.task_dict:
|
||||
|
||||
self.task_dict[name].task.requestInterruption()
|
||||
self.task_dict[name].requestInterruption()
|
||||
self.task_dict[name].quit()
|
||||
self.task_dict[name].wait()
|
||||
|
||||
elif name in self.task_dict:
|
||||
|
||||
self.task_dict[name].task.requestInterruption()
|
||||
self.task_dict[name].requestInterruption()
|
||||
self.task_dict[name].quit()
|
||||
self.task_dict[name].wait()
|
||||
|
||||
def remove_task(self, mode: str, name: str, logs: str):
|
||||
"""任务结束后的处理"""
|
||||
|
||||
logger.info(f"任务结束:{name}")
|
||||
MainInfoBar.push_info_bar("info", "任务结束", name, 3000)
|
||||
|
||||
self.task_dict[name].deleteLater()
|
||||
|
||||
if len(logs) > 0:
|
||||
time = logs[0][1]["Time"]
|
||||
history = ""
|
||||
for log in logs:
|
||||
Config.save_history(log[0], log[1])
|
||||
history += (
|
||||
f"任务名称:{log[0]},{log[1]["History"].replace("\n","\n ")}\n"
|
||||
self.task_dict[script_id] = asyncio.create_task(task_item.run())
|
||||
self.task_dict[script_id].add_done_callback(
|
||||
lambda t: asyncio.create_task(task_item.final_task(t))
|
||||
)
|
||||
Config.save_history(name, {"Time": time, "History": history})
|
||||
self.task_dict[script_id].add_done_callback(
|
||||
partial(self.task_dict.pop, script_id)
|
||||
)
|
||||
try:
|
||||
await self.task_dict[script_id]
|
||||
task["status"] = "完成"
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"任务 {script_id} 运行出错: {type(e).__name__}: {str(e)}"
|
||||
)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id),
|
||||
type="Info",
|
||||
data={
|
||||
"Error": f"任务运行时出错 {type(e).__name__}: {str(e)}"
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
task["status"] = "异常"
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id),
|
||||
type="Update",
|
||||
data={"task_list": task_list},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
async def stop_task(self, task_id: str) -> None:
|
||||
"""
|
||||
中止任务
|
||||
|
||||
:param task_id: 任务ID
|
||||
"""
|
||||
|
||||
logger.info(f"中止任务: {task_id}")
|
||||
|
||||
if task_id == "ALL":
|
||||
for task in self.task_dict.values():
|
||||
task.cancel()
|
||||
else:
|
||||
Config.save_history(
|
||||
name,
|
||||
{
|
||||
"Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"History": "没有任务被执行",
|
||||
},
|
||||
)
|
||||
uid = uuid.UUID(task_id)
|
||||
if uid not in self.task_dict:
|
||||
raise ValueError("任务未在运行")
|
||||
self.task_dict[uid].cancel()
|
||||
|
||||
self.task_dict.pop(name)
|
||||
Config.running_list.remove(name)
|
||||
async def remove_task(
|
||||
self, task: asyncio.Task, mode: str, task_id: uuid.UUID
|
||||
) -> None:
|
||||
"""
|
||||
处理任务结束后的收尾工作
|
||||
|
||||
if "调度队列" in name and "人工排查" not in mode:
|
||||
with (Config.app_path / f"config/QueueConfig/{name}.json").open(
|
||||
"r", encoding="utf-8"
|
||||
) as f:
|
||||
info = json.load(f)
|
||||
System.set_power(info["QueueSet"]["AfterAccomplish"])
|
||||
Parameters
|
||||
----------
|
||||
task : asyncio.Task
|
||||
任务对象
|
||||
mode : str
|
||||
任务模式
|
||||
task_id : uuid.UUID
|
||||
任务ID
|
||||
"""
|
||||
|
||||
def push_dialog(self, name: str, title: str, content: str):
|
||||
"""推送对话框"""
|
||||
logger.info(f"任务结束: {task_id}")
|
||||
|
||||
choice = Dialog(title, content, None)
|
||||
choice.yesButton.setText("是")
|
||||
choice.cancelButton.setText("否")
|
||||
# 从任务字典中移除任务
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"任务 {task_id} 已结束")
|
||||
self.task_dict.pop(task_id)
|
||||
|
||||
self.task_dict[name].question_response.emit(bool(choice.exec_()))
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=str(task_id), type="Signal", data={"Accomplish": "无描述"}
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
if mode == "自动代理" and task_id in Config.QueueConfig:
|
||||
|
||||
if Config.power_sign == "NoAction":
|
||||
Config.power_sign = Config.QueueConfig[task_id].get(
|
||||
"Info", "AfterAccomplish"
|
||||
)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Main", type="Update", data={"PowerSign": Config.power_sign}
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
if len(self.task_dict) == 0 and Config.power_sign != "NoAction":
|
||||
logger.info(f"所有任务已结束,准备执行电源操作: {Config.power_sign}")
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Main",
|
||||
type="Message",
|
||||
data={
|
||||
"type": "Countdown",
|
||||
"title": f"{POWER_SIGN_MAP[Config.power_sign]}倒计时",
|
||||
"message": f"程序将在倒计时结束后执行 {POWER_SIGN_MAP[Config.power_sign]} 操作",
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
await System.start_power_task()
|
||||
|
||||
async def start_startup_queue(self):
|
||||
"""开始运行启动时运行的调度队列"""
|
||||
|
||||
logger.info("开始运行启动时任务")
|
||||
for uid, queue in Config.QueueConfig.items():
|
||||
|
||||
if queue.get("Info", "StartUpEnabled") and uid not in self.task_dict:
|
||||
logger.info(f"启动时需要运行的队列:{uid}")
|
||||
task_id = await TaskManager.add_task("自动代理", str(uid))
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="TaskManager", type="Signal", data={"newTask": str(task_id)}
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
logger.success("启动时任务开始运行")
|
||||
|
||||
|
||||
TaskManager = _TaskManager()
|
||||
|
||||
@@ -1,128 +1,149 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA主业务定时器
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from PySide6.QtCore import QTimer
|
||||
import json
|
||||
import asyncio
|
||||
import keyboard
|
||||
from datetime import datetime
|
||||
import pyautogui
|
||||
|
||||
from .config import Config
|
||||
from app.services import Matomo, System
|
||||
from app.utils import get_logger
|
||||
from app.models.schema import WebSocketMessage
|
||||
from .config import Config, QueueConfig
|
||||
from .task_manager import TaskManager
|
||||
from app.services import System
|
||||
|
||||
|
||||
class _MainTimer(QWidget):
|
||||
logger = get_logger("主业务定时器")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.if_FailSafeException = False
|
||||
class _MainTimer:
|
||||
|
||||
self.Timer = QTimer()
|
||||
self.Timer.timeout.connect(self.timed_start)
|
||||
self.Timer.timeout.connect(self.set_silence)
|
||||
self.Timer.start(1000)
|
||||
async def second_task(self):
|
||||
"""每秒定期任务"""
|
||||
logger.info("每秒定期任务启动")
|
||||
|
||||
def timed_start(self):
|
||||
while True:
|
||||
|
||||
await self.set_silence()
|
||||
await self.timed_start()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def hour_task(self):
|
||||
"""每小时定期任务"""
|
||||
|
||||
logger.info("每小时定期任务启动")
|
||||
|
||||
while True:
|
||||
|
||||
if (
|
||||
datetime.strptime(
|
||||
Config.get("Data", "LastStatisticsUpload"), "%Y-%m-%d %H:%M:%S"
|
||||
).date()
|
||||
!= datetime.now().date()
|
||||
):
|
||||
await Matomo.send_event(
|
||||
"App",
|
||||
"Version",
|
||||
Config.version(),
|
||||
1 if "beta" in Config.version() else 0,
|
||||
)
|
||||
await Config.set(
|
||||
"Data",
|
||||
"LastStatisticsUpload",
|
||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
|
||||
await asyncio.sleep(3600)
|
||||
|
||||
@logger.catch()
|
||||
async def timed_start(self):
|
||||
"""定时启动代理任务"""
|
||||
|
||||
# 获取定时列表
|
||||
queue_list = self.search_queue()
|
||||
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
for i in queue_list:
|
||||
for uid, queue in Config.QueueConfig.items():
|
||||
|
||||
name, info = i
|
||||
|
||||
if not info["QueueSet"]["Enabled"]:
|
||||
if not isinstance(queue, QueueConfig) or not queue.get(
|
||||
"Info", "TimeEnabled"
|
||||
):
|
||||
continue
|
||||
|
||||
history = Config.get_history(name)
|
||||
# 避免重复调起任务
|
||||
if curtime == queue.get("Data", "LastTimedStart"):
|
||||
continue
|
||||
|
||||
time_set = [
|
||||
info["Time"][f"TimeSet_{_}"]
|
||||
for _ in range(10)
|
||||
if info["Time"][f"TimeEnabled_{_}"]
|
||||
]
|
||||
# 按时间调起代理任务
|
||||
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
if (
|
||||
curtime[11:16] in time_set
|
||||
and curtime != history["Time"][:16]
|
||||
and name not in Config.running_list
|
||||
):
|
||||
for time_set in queue.TimeSet.values():
|
||||
if (
|
||||
time_set.get("Info", "Enabled")
|
||||
and curtime[11:16] == time_set.get("Info", "Time")
|
||||
and uid not in Config.task_dict
|
||||
):
|
||||
logger.info(f"定时唤起任务:{uid}")
|
||||
task_id = await TaskManager.add_task("自动代理", str(uid))
|
||||
await queue.set("Data", "LastTimedStart", curtime)
|
||||
await Config.QueueConfig.save()
|
||||
|
||||
logger.info(f"定时任务:{name}")
|
||||
TaskManager.add_task("自动代理_新调度台", name, info)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="TaskManager",
|
||||
type="Signal",
|
||||
data={"newTask": str(task_id)},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
def set_silence(self):
|
||||
"""设置静默模式"""
|
||||
@logger.catch()
|
||||
async def set_silence(self):
|
||||
"""静默模式通过模拟老板键来隐藏模拟器窗口"""
|
||||
|
||||
if (
|
||||
Config.global_config.get(Config.global_config.function_IfSilence)
|
||||
and Config.global_config.get(Config.global_config.function_BossKey) != ""
|
||||
len(Config.if_ignore_silence) == 0
|
||||
and Config.get("Function", "IfSilence")
|
||||
and Config.get("Function", "BossKey") != ""
|
||||
):
|
||||
|
||||
windows = System.get_window_info()
|
||||
if any(
|
||||
str(emulator_path) in window
|
||||
for window in windows
|
||||
for emulator_path in Config.silence_list
|
||||
):
|
||||
windows = await System.get_window_info()
|
||||
|
||||
emulator_windows = []
|
||||
for window in windows:
|
||||
for emulator_path, endtime in Config.silence_dict.items():
|
||||
if (
|
||||
datetime.now() < endtime
|
||||
and str(emulator_path) in window
|
||||
and window[0] != "新通知" # 此处排除雷电名为新通知的窗口
|
||||
):
|
||||
emulator_windows.append(window)
|
||||
|
||||
if emulator_windows:
|
||||
|
||||
logger.info(f"检测到模拟器窗口: {emulator_windows}")
|
||||
try:
|
||||
pyautogui.hotkey(
|
||||
*[
|
||||
keyboard.press_and_release(
|
||||
"+".join(
|
||||
_.strip().lower()
|
||||
for _ in Config.global_config.get(
|
||||
Config.global_config.function_BossKey
|
||||
).split("+")
|
||||
]
|
||||
for _ in Config.get("Function", "BossKey").split("+")
|
||||
)
|
||||
)
|
||||
except pyautogui.FailSafeException as e:
|
||||
if not self.if_FailSafeException:
|
||||
logger.warning(f"FailSafeException: {e}")
|
||||
self.if_FailSafeException = True
|
||||
|
||||
def search_queue(self) -> list:
|
||||
"""搜索所有调度队列实例"""
|
||||
|
||||
queue_list = []
|
||||
|
||||
if (Config.app_path / "config/QueueConfig").exists():
|
||||
for json_file in (Config.app_path / "config/QueueConfig").glob("*.json"):
|
||||
with json_file.open("r", encoding="utf-8") as f:
|
||||
info = json.load(f)
|
||||
queue_list.append([json_file.stem, info])
|
||||
|
||||
return queue_list
|
||||
logger.info(f"模拟按键: {Config.get('Function', 'BossKey')}")
|
||||
except Exception as e:
|
||||
logger.exception(f"模拟按键时出错: {e}")
|
||||
|
||||
|
||||
MainTimer = _MainTimer()
|
||||
|
||||
859
app/models/ConfigBase.py
Normal file
859
app/models/ConfigBase.py
Normal file
@@ -0,0 +1,859 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import win32com.client
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Any, Dict, Union, Optional
|
||||
|
||||
|
||||
from app.utils import dpapi_encrypt, dpapi_decrypt
|
||||
from app.utils.constants import RESERVED_NAMES, ILLEGAL_CHARS, DEFAULT_DATETIME
|
||||
|
||||
|
||||
class ConfigValidator:
|
||||
"""基础配置验证器"""
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
"""验证值是否合法"""
|
||||
return True
|
||||
|
||||
def correct(self, value: Any) -> Any:
|
||||
"""修正非法值"""
|
||||
return value
|
||||
|
||||
|
||||
class RangeValidator(ConfigValidator):
|
||||
"""范围验证器"""
|
||||
|
||||
def __init__(self, min: int | float, max: int | float):
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.range = (min, max)
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if not isinstance(value, (int | float)):
|
||||
return False
|
||||
return self.min <= value <= self.max
|
||||
|
||||
def correct(self, value: Any) -> int | float:
|
||||
if not isinstance(value, (int, float)):
|
||||
try:
|
||||
value = float(value)
|
||||
except TypeError:
|
||||
return self.min
|
||||
return min(max(self.min, value), self.max)
|
||||
|
||||
|
||||
class OptionsValidator(ConfigValidator):
|
||||
"""选项验证器"""
|
||||
|
||||
def __init__(self, options: list):
|
||||
if not options:
|
||||
raise ValueError("可选项不能为空")
|
||||
|
||||
self.options = options
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
return value in self.options
|
||||
|
||||
def correct(self, value: Any) -> Any:
|
||||
return value if self.validate(value) else self.options[0]
|
||||
|
||||
|
||||
class UUIDValidator(ConfigValidator):
|
||||
"""UUID验证器"""
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
try:
|
||||
uuid.UUID(value)
|
||||
return True
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
def correct(self, value: Any) -> Any:
|
||||
return value if self.validate(value) else str(uuid.uuid4())
|
||||
|
||||
|
||||
class DateTimeValidator(ConfigValidator):
|
||||
"""日期时间验证器"""
|
||||
|
||||
def __init__(self, date_format: str) -> None:
|
||||
if not date_format:
|
||||
raise ValueError("日期时间格式不能为空")
|
||||
self.date_format = date_format
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
try:
|
||||
datetime.strptime(value, self.date_format)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def correct(self, value: Any) -> str:
|
||||
if not isinstance(value, str):
|
||||
return DEFAULT_DATETIME.strftime(self.date_format)
|
||||
try:
|
||||
datetime.strptime(value, self.date_format)
|
||||
return value
|
||||
except ValueError:
|
||||
return DEFAULT_DATETIME.strftime(self.date_format)
|
||||
|
||||
|
||||
class JSONValidator(ConfigValidator):
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
try:
|
||||
json.loads(value)
|
||||
return True
|
||||
except json.JSONDecodeError:
|
||||
return False
|
||||
|
||||
def correct(self, value: Any) -> str:
|
||||
return value if self.validate(value) else "{ }"
|
||||
|
||||
|
||||
class EncryptValidator(ConfigValidator):
|
||||
"""加密数据验证器"""
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
try:
|
||||
dpapi_decrypt(value)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def correct(self, value: Any) -> Any:
|
||||
return value if self.validate(value) else dpapi_encrypt("数据损坏, 请重新设置")
|
||||
|
||||
|
||||
class BoolValidator(OptionsValidator):
|
||||
"""布尔值验证器"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__([True, False])
|
||||
|
||||
|
||||
class FileValidator(ConfigValidator):
|
||||
"""文件路径验证器"""
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
if not Path(value).is_absolute():
|
||||
return False
|
||||
if Path(value).suffix == ".lnk":
|
||||
return False
|
||||
return True
|
||||
|
||||
def correct(self, value: Any) -> str:
|
||||
if not isinstance(value, str):
|
||||
value = str(Path.cwd())
|
||||
if not Path(value).is_absolute():
|
||||
value = Path(value).resolve().as_posix()
|
||||
if Path(value).suffix == ".lnk":
|
||||
try:
|
||||
shell = win32com.client.Dispatch("WScript.Shell")
|
||||
shortcut = shell.CreateShortcut(value)
|
||||
value = shortcut.TargetPath
|
||||
except:
|
||||
pass
|
||||
return Path(value).resolve().as_posix()
|
||||
|
||||
|
||||
class FolderValidator(ConfigValidator):
|
||||
"""文件夹路径验证器"""
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
if not Path(value).is_absolute():
|
||||
return False
|
||||
return True
|
||||
|
||||
def correct(self, value: Any) -> str:
|
||||
if not isinstance(value, str):
|
||||
value = str(Path.cwd())
|
||||
return Path(value).resolve().as_posix()
|
||||
|
||||
|
||||
class UserNameValidator(ConfigValidator):
|
||||
"""用户名验证器"""
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
|
||||
if not value or not value.strip():
|
||||
return False
|
||||
|
||||
if value != value.strip() or value != value.strip("."):
|
||||
return False
|
||||
|
||||
if any(char in ILLEGAL_CHARS for char in value):
|
||||
return False
|
||||
|
||||
if value.upper() in RESERVED_NAMES:
|
||||
return False
|
||||
if len(value) > 255:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def correct(self, value: Any) -> str:
|
||||
if not isinstance(value, str):
|
||||
value = "默认用户名"
|
||||
|
||||
value = value.strip().strip(".")
|
||||
|
||||
value = "".join(char for char in value if char not in ILLEGAL_CHARS)
|
||||
|
||||
if value.upper() in RESERVED_NAMES or not value:
|
||||
value = "默认用户名"
|
||||
|
||||
if len(value) > 255:
|
||||
value = value[:255]
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class ConfigItem:
|
||||
"""配置项"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
group: str,
|
||||
name: str,
|
||||
default: Any,
|
||||
validator: Optional[ConfigValidator] = None,
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
group: str
|
||||
配置项分组名称
|
||||
|
||||
name: str
|
||||
配置项字段名称
|
||||
|
||||
default: Any
|
||||
配置项默认值
|
||||
|
||||
validator: ConfigValidator
|
||||
配置项验证器, 默认为 None, 表示不进行验证
|
||||
"""
|
||||
super().__init__()
|
||||
self.group = group
|
||||
self.name = name
|
||||
self.value: Any = default
|
||||
self.validator = validator or ConfigValidator()
|
||||
self.is_locked = False
|
||||
|
||||
if not self.validator.validate(self.value):
|
||||
raise ValueError(
|
||||
f"配置项 '{self.group}.{self.name}' 的默认值 '{self.value}' 不合法"
|
||||
)
|
||||
|
||||
def setValue(self, value: Any):
|
||||
"""
|
||||
设置配置项值, 将自动进行验证和修正
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value: Any
|
||||
要设置的值, 可以是任何合法类型
|
||||
"""
|
||||
|
||||
if (
|
||||
dpapi_decrypt(self.value)
|
||||
if isinstance(self.validator, EncryptValidator)
|
||||
else self.value
|
||||
) == value:
|
||||
return
|
||||
|
||||
if self.is_locked:
|
||||
raise ValueError(f"配置项 '{self.group}.{self.name}' 已锁定, 无法修改")
|
||||
|
||||
# deepcopy new value
|
||||
try:
|
||||
self.value = deepcopy(value)
|
||||
except:
|
||||
self.value = value
|
||||
|
||||
if isinstance(self.validator, EncryptValidator):
|
||||
if self.validator.validate(self.value):
|
||||
self.value = self.value
|
||||
else:
|
||||
self.value = dpapi_encrypt(self.value)
|
||||
|
||||
if not self.validator.validate(self.value):
|
||||
self.value = self.validator.correct(self.value)
|
||||
|
||||
def getValue(self, if_decrypt: bool = True) -> Any:
|
||||
"""
|
||||
获取配置项值
|
||||
"""
|
||||
|
||||
v = (
|
||||
self.value
|
||||
if self.validator.validate(self.value)
|
||||
else self.validator.correct(self.value)
|
||||
)
|
||||
|
||||
if isinstance(self.validator, EncryptValidator) and if_decrypt:
|
||||
return dpapi_decrypt(v)
|
||||
return v
|
||||
|
||||
def lock(self):
|
||||
"""
|
||||
锁定配置项, 锁定后无法修改配置项值
|
||||
"""
|
||||
self.is_locked = True
|
||||
|
||||
def unlock(self):
|
||||
"""
|
||||
解锁配置项, 解锁后可以修改配置项值
|
||||
"""
|
||||
self.is_locked = False
|
||||
|
||||
|
||||
class ConfigBase:
|
||||
"""
|
||||
配置基类
|
||||
|
||||
这个类提供了基本的配置项管理功能, 包括连接配置文件、加载配置数据、获取和设置配置项值等。
|
||||
|
||||
此类不支持直接实例化, 必须通过子类来实现具体的配置项, 请继承此类并在子类中定义具体的配置项。
|
||||
若将配置项设为类属性, 则所有实例都会共享同一份配置项数据。
|
||||
若将配置项设为实例属性, 则每个实例都会有独立的配置项数据。
|
||||
子配置项可以是 `MultipleConfig` 的实例。
|
||||
"""
|
||||
|
||||
def __init__(self, if_save_multi_config: bool = True):
|
||||
|
||||
self.file: Optional[Path] = None
|
||||
self.if_save_multi_config = if_save_multi_config
|
||||
self.is_locked = False
|
||||
|
||||
async def connect(self, path: Path):
|
||||
"""
|
||||
将配置文件连接到指定配置文件
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path: Path
|
||||
配置文件路径, 必须为 JSON 文件, 如果不存在则会创建
|
||||
"""
|
||||
|
||||
if path.suffix != ".json":
|
||||
raise ValueError("配置文件必须是扩展名为 '.json' 的 JSON 文件")
|
||||
|
||||
if self.is_locked:
|
||||
raise ValueError("配置已锁定, 无法修改")
|
||||
|
||||
self.file = path
|
||||
|
||||
if not self.file.exists():
|
||||
self.file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.file.touch()
|
||||
|
||||
try:
|
||||
data = json.loads(self.file.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
data = {}
|
||||
|
||||
await self.load(data)
|
||||
|
||||
async def load(self, data: dict):
|
||||
"""
|
||||
从字典加载配置数据
|
||||
|
||||
这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigItem 实例中。
|
||||
如果字典中包含 "SubConfigsInfo" 键, 则会加载子配置项, 这些子配置项应该是 MultipleConfig 的实例。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data: dict
|
||||
配置数据字典
|
||||
"""
|
||||
|
||||
if self.is_locked:
|
||||
raise ValueError("配置已锁定, 无法修改")
|
||||
|
||||
# update the value of config item
|
||||
if data.get("SubConfigsInfo"):
|
||||
for k, v in data["SubConfigsInfo"].items():
|
||||
if hasattr(self, k):
|
||||
sub_config = getattr(self, k)
|
||||
if isinstance(sub_config, MultipleConfig):
|
||||
await sub_config.load(v)
|
||||
data.pop("SubConfigsInfo")
|
||||
|
||||
for group, info in data.items():
|
||||
for name, value in info.items():
|
||||
if hasattr(self, f"{group}_{name}"):
|
||||
configItem = getattr(self, f"{group}_{name}")
|
||||
if isinstance(configItem, ConfigItem):
|
||||
configItem.setValue(value)
|
||||
|
||||
if self.file:
|
||||
await self.save()
|
||||
|
||||
async def toDict(
|
||||
self, ignore_multi_config: bool = False, if_decrypt: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""将配置项转换为字典"""
|
||||
|
||||
data = {}
|
||||
for name in dir(self):
|
||||
item = getattr(self, name)
|
||||
|
||||
if isinstance(item, ConfigItem):
|
||||
|
||||
if not data.get(item.group):
|
||||
data[item.group] = {}
|
||||
if item.name:
|
||||
data[item.group][item.name] = item.getValue(if_decrypt)
|
||||
|
||||
elif not ignore_multi_config and isinstance(item, MultipleConfig):
|
||||
|
||||
if not data.get("SubConfigsInfo"):
|
||||
data["SubConfigsInfo"] = {}
|
||||
data["SubConfigsInfo"][name] = await item.toDict()
|
||||
|
||||
return data
|
||||
|
||||
def get(self, group: str, name: str) -> Any:
|
||||
"""获取配置项的值"""
|
||||
|
||||
if not hasattr(self, f"{group}_{name}"):
|
||||
raise AttributeError(f"配置项 '{group}.{name}' 不存在")
|
||||
|
||||
configItem = getattr(self, f"{group}_{name}")
|
||||
if isinstance(configItem, ConfigItem):
|
||||
return configItem.getValue()
|
||||
else:
|
||||
raise TypeError(f"配置项 '{group}.{name}' 不是 ConfigItem 实例")
|
||||
|
||||
async def set(self, group: str, name: str, value: Any):
|
||||
"""
|
||||
设置配置项的值
|
||||
|
||||
Parameters
|
||||
----------
|
||||
group: str
|
||||
配置项分组名称
|
||||
name: str
|
||||
配置项名称
|
||||
value: Any
|
||||
配置项新值
|
||||
"""
|
||||
|
||||
if not hasattr(self, f"{group}_{name}"):
|
||||
raise AttributeError(f"配置项 '{group}.{name}' 不存在")
|
||||
|
||||
if self.is_locked:
|
||||
raise ValueError("配置已锁定, 无法修改")
|
||||
|
||||
configItem = getattr(self, f"{group}_{name}")
|
||||
if isinstance(configItem, ConfigItem):
|
||||
configItem.setValue(value)
|
||||
if self.file:
|
||||
await self.save()
|
||||
else:
|
||||
raise TypeError(f"配置项 '{group}.{name}' 不是 ConfigItem 实例")
|
||||
|
||||
async def save(self):
|
||||
"""保存配置"""
|
||||
|
||||
if not self.file:
|
||||
raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件")
|
||||
|
||||
self.file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.file.write_text(
|
||||
json.dumps(
|
||||
await self.toDict(not self.if_save_multi_config, if_decrypt=False),
|
||||
ensure_ascii=False,
|
||||
indent=4,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
async def lock(self):
|
||||
"""
|
||||
锁定配置项, 锁定后无法修改配置项值
|
||||
"""
|
||||
|
||||
self.is_locked = True
|
||||
|
||||
for name in dir(self):
|
||||
item = getattr(self, name)
|
||||
if isinstance(item, ConfigItem):
|
||||
item.lock()
|
||||
elif isinstance(item, MultipleConfig):
|
||||
await item.lock()
|
||||
|
||||
async def unlock(self):
|
||||
"""
|
||||
解锁配置项, 解锁后可以修改配置项值
|
||||
"""
|
||||
|
||||
self.is_locked = False
|
||||
|
||||
for name in dir(self):
|
||||
item = getattr(self, name)
|
||||
if isinstance(item, ConfigItem):
|
||||
item.unlock()
|
||||
elif isinstance(item, MultipleConfig):
|
||||
await item.unlock()
|
||||
|
||||
|
||||
class MultipleConfig:
|
||||
"""
|
||||
多配置项管理类
|
||||
|
||||
这个类允许管理多个配置项实例, 可以添加、删除、修改配置项, 并将其保存到 JSON 文件中。
|
||||
允许通过 `config[uuid]` 访问配置项, 使用 `uuid in config` 检查是否存在配置项, 使用 `len(config)` 获取配置项数量。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sub_config_type: List[type]
|
||||
子配置项的类型列表, 必须是 ConfigBase 的子类
|
||||
"""
|
||||
|
||||
def __init__(self, sub_config_type: List[type]):
|
||||
|
||||
if not sub_config_type:
|
||||
raise ValueError("子配置项类型列表不能为空")
|
||||
|
||||
for config_type in sub_config_type:
|
||||
if not issubclass(config_type, ConfigBase):
|
||||
raise TypeError(
|
||||
f"配置类型 {config_type.__name__} 必须是 ConfigBase 的子类"
|
||||
)
|
||||
|
||||
self.sub_config_type = sub_config_type
|
||||
self.file: None | Path = None
|
||||
self.order: List[uuid.UUID] = []
|
||||
self.data: Dict[uuid.UUID, ConfigBase] = {}
|
||||
self.is_locked = False
|
||||
|
||||
def __getitem__(self, key: uuid.UUID) -> ConfigBase:
|
||||
"""允许通过 config[uuid] 访问配置项"""
|
||||
if key not in self.data:
|
||||
raise KeyError(f"配置项 '{key}' 不存在")
|
||||
return self.data[key]
|
||||
|
||||
def __contains__(self, key: uuid.UUID) -> bool:
|
||||
"""允许使用 uuid in config 检查是否存在"""
|
||||
return key in self.data
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""允许使用 len(config) 获取配置项数量"""
|
||||
return len(self.data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""更好的字符串表示"""
|
||||
return f"MultipleConfig(items={len(self.data)}, types={[t.__name__ for t in self.sub_config_type]})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""用户友好的字符串表示"""
|
||||
return f"MultipleConfig with {len(self.data)} items"
|
||||
|
||||
async def connect(self, path: Path):
|
||||
"""
|
||||
将配置文件连接到指定配置文件
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path: Path
|
||||
配置文件路径, 必须为 JSON 文件, 如果不存在则会创建
|
||||
"""
|
||||
|
||||
if path.suffix != ".json":
|
||||
raise ValueError("配置文件必须是带有 '.json' 扩展名的 JSON 文件。")
|
||||
|
||||
if self.is_locked:
|
||||
raise ValueError("配置已锁定, 无法修改")
|
||||
|
||||
self.file = path
|
||||
|
||||
if not self.file.exists():
|
||||
self.file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.file.touch()
|
||||
|
||||
try:
|
||||
data = json.loads(self.file.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
data = {}
|
||||
|
||||
await self.load(data)
|
||||
|
||||
async def load(self, data: dict):
|
||||
"""
|
||||
从字典加载配置数据
|
||||
|
||||
这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigBase 实例中。
|
||||
如果字典中包含 "instances" 键, 则会加载子配置项, 这些子配置项应该是 ConfigBase 子类的实例。
|
||||
如果字典中没有 "instances" 键, 则清空当前配置项。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data: dict
|
||||
配置数据字典
|
||||
"""
|
||||
|
||||
if self.is_locked:
|
||||
raise ValueError("配置已锁定, 无法修改")
|
||||
|
||||
if not data.get("instances"):
|
||||
self.order = []
|
||||
self.data = {}
|
||||
return
|
||||
|
||||
self.order = []
|
||||
self.data = {}
|
||||
|
||||
for instance in data["instances"]:
|
||||
|
||||
if not isinstance(instance, dict) or not data.get(instance.get("uid")):
|
||||
continue
|
||||
|
||||
type_name = instance.get("type", self.sub_config_type[0].__name__)
|
||||
|
||||
for class_type in self.sub_config_type:
|
||||
|
||||
if class_type.__name__ == type_name:
|
||||
self.order.append(uuid.UUID(instance["uid"]))
|
||||
self.data[self.order[-1]] = class_type()
|
||||
await self.data[self.order[-1]].load(data[instance["uid"]])
|
||||
break
|
||||
|
||||
else:
|
||||
|
||||
raise ValueError(f"未知的子配置类型: {type_name}")
|
||||
|
||||
if self.file:
|
||||
await self.save()
|
||||
|
||||
async def toDict(self) -> Dict[str, Union[list, dict]]:
|
||||
"""
|
||||
将配置项转换为字典
|
||||
|
||||
返回一个字典, 包含所有配置项的 UID 和类型, 以及每个配置项的具体数据。
|
||||
"""
|
||||
|
||||
data: Dict[str, Union[list, dict]] = {
|
||||
"instances": [
|
||||
{"uid": str(_), "type": type(self.data[_]).__name__} for _ in self.order
|
||||
]
|
||||
}
|
||||
for uid, config in self.items():
|
||||
data[str(uid)] = await config.toDict()
|
||||
return data
|
||||
|
||||
async def get(self, uid: uuid.UUID) -> Dict[str, Union[list, dict]]:
|
||||
"""
|
||||
获取指定 UID 的配置项
|
||||
|
||||
Parameters
|
||||
----------
|
||||
uid: uuid.UUID
|
||||
要获取的配置项的唯一标识符
|
||||
Returns
|
||||
-------
|
||||
Dict[str, Union[list, dict]]
|
||||
对应的配置项数据字典
|
||||
"""
|
||||
|
||||
if uid not in self.data:
|
||||
raise ValueError(f"配置项 '{uid}' 不存在。")
|
||||
|
||||
data: Dict[str, Union[list, dict]] = {
|
||||
"instances": [
|
||||
{"uid": str(_), "type": type(self.data[_]).__name__}
|
||||
for _ in self.order
|
||||
if _ == uid
|
||||
]
|
||||
}
|
||||
data[str(uid)] = await self.data[uid].toDict()
|
||||
|
||||
return data
|
||||
|
||||
async def save(self):
|
||||
"""保存配置"""
|
||||
|
||||
if not self.file:
|
||||
raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件")
|
||||
|
||||
self.file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.file.write_text(
|
||||
json.dumps(await self.toDict(), ensure_ascii=False, indent=4),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
async def add(self, config_type: type) -> tuple[uuid.UUID, ConfigBase]:
|
||||
"""
|
||||
添加一个新的配置项
|
||||
|
||||
Parameters
|
||||
----------
|
||||
config_type: type
|
||||
配置项的类型, 必须是初始化时已声明的 ConfigBase 子类
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[uuid.UUID, ConfigBase]
|
||||
新创建的配置项的唯一标识符和实例
|
||||
"""
|
||||
|
||||
if config_type not in self.sub_config_type:
|
||||
raise ValueError(f"配置类型 {config_type.__name__} 不被允许")
|
||||
|
||||
uid = uuid.uuid4()
|
||||
self.order.append(uid)
|
||||
self.data[uid] = config_type()
|
||||
|
||||
if self.file:
|
||||
await self.save()
|
||||
|
||||
return uid, self.data[uid]
|
||||
|
||||
async def remove(self, uid: uuid.UUID):
|
||||
"""
|
||||
移除配置项
|
||||
|
||||
Parameters
|
||||
----------
|
||||
uid: uuid.UUID
|
||||
要移除的配置项的唯一标识符
|
||||
"""
|
||||
|
||||
if self.is_locked:
|
||||
raise ValueError("配置已锁定, 无法修改")
|
||||
|
||||
if uid not in self.data:
|
||||
raise ValueError(f"配置项 '{uid}' 不存在")
|
||||
|
||||
if self.data[uid].is_locked:
|
||||
raise ValueError(f"配置项 '{uid}' 已锁定, 无法移除")
|
||||
|
||||
self.data.pop(uid)
|
||||
self.order.remove(uid)
|
||||
|
||||
if self.file:
|
||||
await self.save()
|
||||
|
||||
async def setOrder(self, order: List[uuid.UUID]):
|
||||
"""
|
||||
设置配置项的顺序
|
||||
|
||||
Parameters
|
||||
----------
|
||||
order: List[uuid.UUID]
|
||||
新的配置项顺序
|
||||
"""
|
||||
|
||||
if set(order) != set(self.data.keys()):
|
||||
raise ValueError("顺序与当前配置项不匹配")
|
||||
|
||||
self.order = order
|
||||
|
||||
if self.file:
|
||||
await self.save()
|
||||
|
||||
async def lock(self):
|
||||
"""
|
||||
锁定配置项, 锁定后无法修改配置项值
|
||||
"""
|
||||
|
||||
self.is_locked = True
|
||||
|
||||
for item in self.values():
|
||||
await item.lock()
|
||||
|
||||
async def unlock(self):
|
||||
"""
|
||||
解锁配置项, 解锁后可以修改配置项值
|
||||
"""
|
||||
|
||||
self.is_locked = False
|
||||
|
||||
for item in self.values():
|
||||
await item.unlock()
|
||||
|
||||
def keys(self):
|
||||
"""返回配置项的所有唯一标识符"""
|
||||
|
||||
return iter(self.order)
|
||||
|
||||
def values(self):
|
||||
"""返回配置项的所有实例"""
|
||||
|
||||
if not self.data:
|
||||
return iter([])
|
||||
|
||||
return iter([self.data[_] for _ in self.order])
|
||||
|
||||
def items(self):
|
||||
"""返回配置项的所有唯一标识符和实例的元组"""
|
||||
|
||||
return zip(self.keys(), self.values())
|
||||
|
||||
|
||||
class MultipleUIDValidator(ConfigValidator):
|
||||
"""多配置管理类UID验证器"""
|
||||
|
||||
def __init__(
|
||||
self, default: Any, related_config: Dict[str, MultipleConfig], config_name: str
|
||||
):
|
||||
self.default = default
|
||||
self.related_config = related_config
|
||||
self.config_name = config_name
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if value == self.default:
|
||||
return True
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
try:
|
||||
uid = uuid.UUID(value)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
if uid in self.related_config.get(self.config_name, {}):
|
||||
return True
|
||||
return False
|
||||
|
||||
def correct(self, value: Any) -> Any:
|
||||
if self.validate(value):
|
||||
return value
|
||||
return self.default
|
||||
1120
app/models/MAA.py
1120
app/models/MAA.py
File diff suppressed because it is too large
Load Diff
@@ -1,34 +1,30 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA模组包
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
__version__ = "4.2.0"
|
||||
__version__ = "5.0.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .MAA import MaaManager
|
||||
from .ConfigBase import *
|
||||
from .schema import *
|
||||
|
||||
__all__ = ["MaaManager"]
|
||||
__all__ = ["ConfigBase", "schema"]
|
||||
|
||||
822
app/models/schema.py
Normal file
822
app/models/schema.py
Normal file
@@ -0,0 +1,822 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Any, Dict, List, Union, Optional, Literal
|
||||
|
||||
|
||||
class OutBase(BaseModel):
|
||||
code: int = Field(default=200, description="状态码")
|
||||
status: str = Field(default="success", description="操作状态")
|
||||
message: str = Field(default="操作成功", description="操作消息")
|
||||
|
||||
|
||||
class InfoOut(OutBase):
|
||||
data: Dict[str, Any] = Field(..., description="收到的服务器数据")
|
||||
|
||||
|
||||
class VersionOut(OutBase):
|
||||
if_need_update: bool = Field(..., description="后端代码是否需要更新")
|
||||
current_hash: str = Field(..., description="后端代码当前哈希值")
|
||||
current_time: str = Field(..., description="后端代码当前时间戳")
|
||||
current_version: str = Field(..., description="后端当前版本号")
|
||||
|
||||
|
||||
class NoticeOut(OutBase):
|
||||
if_need_show: bool = Field(..., description="是否需要显示公告")
|
||||
data: Dict[str, str] = Field(
|
||||
..., description="公告信息, key为公告标题, value为公告内容"
|
||||
)
|
||||
|
||||
|
||||
class ComboBoxItem(BaseModel):
|
||||
label: str = Field(..., description="展示值")
|
||||
value: Optional[str] = Field(..., description="实际值")
|
||||
|
||||
|
||||
class ComboBoxOut(OutBase):
|
||||
data: List[ComboBoxItem] = Field(..., description="下拉框选项")
|
||||
|
||||
|
||||
class GetStageIn(BaseModel):
|
||||
type: Literal[
|
||||
"Today",
|
||||
"ALL",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
] = Field(
|
||||
...,
|
||||
description="选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项",
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig_Function(BaseModel):
|
||||
HistoryRetentionTime: Optional[Literal[7, 15, 30, 60, 90, 180, 365, 0]] = Field(
|
||||
None, description="历史记录保留时间, 0表示永久保存"
|
||||
)
|
||||
IfAllowSleep: Optional[bool] = Field(default=None, description="允许休眠")
|
||||
IfSilence: Optional[bool] = Field(default=None, description="静默模式")
|
||||
BossKey: Optional[str] = Field(default=None, description="模拟器老板键")
|
||||
IfAgreeBilibili: Optional[bool] = Field(
|
||||
default=None, description="同意哔哩哔哩用户协议"
|
||||
)
|
||||
IfSkipMumuSplashAds: Optional[bool] = Field(
|
||||
default=None, description="跳过Mumu模拟器启动广告"
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig_Voice(BaseModel):
|
||||
Enabled: Optional[bool] = Field(default=None, description="语音功能是否启用")
|
||||
Type: Optional[Literal["simple", "noisy"]] = Field(
|
||||
default=None, description="语音类型, simple为简洁, noisy为聒噪"
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig_Start(BaseModel):
|
||||
IfSelfStart: Optional[bool] = Field(
|
||||
default=None, description="是否在系统启动时自动运行"
|
||||
)
|
||||
IfMinimizeDirectly: Optional[bool] = Field(
|
||||
default=None, description="启动时是否直接最小化到托盘而不显示主窗口"
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig_UI(BaseModel):
|
||||
IfShowTray: Optional[bool] = Field(default=None, description="是否常态显示托盘图标")
|
||||
IfToTray: Optional[bool] = Field(default=None, description="是否最小化到托盘")
|
||||
|
||||
|
||||
class CustomWebhook(BaseModel):
|
||||
id: str = Field(..., description="Webhook唯一标识")
|
||||
name: str = Field(..., description="Webhook名称")
|
||||
url: str = Field(..., description="Webhook URL")
|
||||
template: str = Field(..., description="消息模板")
|
||||
enabled: bool = Field(default=True, description="是否启用")
|
||||
headers: Optional[Dict[str, str]] = Field(default=None, description="自定义请求头")
|
||||
method: Optional[Literal["POST", "GET"]] = Field(
|
||||
default="POST", description="请求方法"
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig_Notify(BaseModel):
|
||||
SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field(
|
||||
default=None, description="任务结果推送时机"
|
||||
)
|
||||
IfSendStatistic: Optional[bool] = Field(
|
||||
default=None, description="是否发送统计信息"
|
||||
)
|
||||
IfSendSixStar: Optional[bool] = Field(
|
||||
default=None, description="是否发送公招六星通知"
|
||||
)
|
||||
IfPushPlyer: Optional[bool] = Field(default=None, description="是否推送系统通知")
|
||||
IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知")
|
||||
SMTPServerAddress: Optional[str] = Field(default=None, description="SMTP服务器地址")
|
||||
AuthorizationCode: Optional[str] = Field(default=None, description="SMTP授权码")
|
||||
FromAddress: Optional[str] = Field(default=None, description="邮件发送地址")
|
||||
ToAddress: Optional[str] = Field(default=None, description="邮件接收地址")
|
||||
IfServerChan: Optional[bool] = Field(
|
||||
default=None, description="是否使用ServerChan推送"
|
||||
)
|
||||
ServerChanKey: Optional[str] = Field(default=None, description="ServerChan推送密钥")
|
||||
CustomWebhooks: Optional[List[CustomWebhook]] = Field(
|
||||
default=None, description="自定义Webhook列表"
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig_Update(BaseModel):
|
||||
IfAutoUpdate: Optional[bool] = Field(default=None, description="是否自动更新")
|
||||
Source: Optional[Literal["GitHub", "MirrorChyan", "AutoSite"]] = Field(
|
||||
default=None, description="更新源: GitHub源, Mirror酱源, 自建源"
|
||||
)
|
||||
ProxyAddress: Optional[str] = Field(default=None, description="网络代理地址")
|
||||
MirrorChyanCDK: Optional[str] = Field(default=None, description="Mirror酱CDK")
|
||||
|
||||
|
||||
class GlobalConfig(BaseModel):
|
||||
Function: Optional[GlobalConfig_Function] = Field(
|
||||
default=None, description="功能相关配置"
|
||||
)
|
||||
Voice: Optional[GlobalConfig_Voice] = Field(
|
||||
default=None, description="语音相关配置"
|
||||
)
|
||||
Start: Optional[GlobalConfig_Start] = Field(
|
||||
default=None, description="启动相关配置"
|
||||
)
|
||||
UI: Optional[GlobalConfig_UI] = Field(default=None, description="界面相关配置")
|
||||
Notify: Optional[GlobalConfig_Notify] = Field(
|
||||
default=None, description="通知相关配置"
|
||||
)
|
||||
Update: Optional[GlobalConfig_Update] = Field(
|
||||
default=None, description="更新相关配置"
|
||||
)
|
||||
|
||||
|
||||
class QueueIndexItem(BaseModel):
|
||||
uid: str = Field(..., description="唯一标识符")
|
||||
type: Literal["QueueConfig"] = Field(..., description="配置类型")
|
||||
|
||||
|
||||
class QueueItemIndexItem(BaseModel):
|
||||
uid: str = Field(..., description="唯一标识符")
|
||||
type: Literal["QueueItem"] = Field(..., description="配置类型")
|
||||
|
||||
|
||||
class TimeSetIndexItem(BaseModel):
|
||||
uid: str = Field(..., description="唯一标识符")
|
||||
type: Literal["TimeSet"] = Field(..., description="配置类型")
|
||||
|
||||
|
||||
class QueueItem_Info(BaseModel):
|
||||
ScriptId: Optional[str] = Field(
|
||||
default=None, description="任务所对应的脚本ID, 为None时表示未选择"
|
||||
)
|
||||
|
||||
|
||||
class QueueItem(BaseModel):
|
||||
Info: Optional[QueueItem_Info] = Field(default=None, description="队列项")
|
||||
|
||||
|
||||
class TimeSet_Info(BaseModel):
|
||||
Enabled: Optional[bool] = Field(default=None, description="是否启用")
|
||||
Time: Optional[str] = Field(default=None, description="时间设置, 格式为HH:MM")
|
||||
|
||||
|
||||
class TimeSet(BaseModel):
|
||||
Info: Optional[TimeSet_Info] = Field(default=None, description="时间项")
|
||||
|
||||
|
||||
class QueueConfig_Info(BaseModel):
|
||||
Name: Optional[str] = Field(default=None, description="队列名称")
|
||||
TimeEnabled: Optional[bool] = Field(default=None, description="是否启用定时")
|
||||
StartUpEnabled: Optional[bool] = Field(default=None, description="是否启动时运行")
|
||||
AfterAccomplish: Optional[
|
||||
Literal[
|
||||
"NoAction", "KillSelf", "Sleep", "Hibernate", "Shutdown", "ShutdownForce"
|
||||
]
|
||||
] = Field(default=None, description="完成后操作")
|
||||
|
||||
|
||||
class QueueConfig(BaseModel):
|
||||
Info: Optional[QueueConfig_Info] = Field(default=None, description="队列信息")
|
||||
|
||||
|
||||
class ScriptIndexItem(BaseModel):
|
||||
uid: str = Field(..., description="唯一标识符")
|
||||
type: Literal["MaaConfig", "GeneralConfig"] = Field(..., description="配置类型")
|
||||
|
||||
|
||||
class UserIndexItem(BaseModel):
|
||||
uid: str = Field(..., description="唯一标识符")
|
||||
type: Literal["MaaUserConfig", "GeneralUserConfig"] = Field(
|
||||
..., description="配置类型"
|
||||
)
|
||||
|
||||
|
||||
class MaaUserConfig_Info(BaseModel):
|
||||
Name: Optional[str] = Field(default=None, description="用户名")
|
||||
Id: Optional[str] = Field(default=None, description="用户ID")
|
||||
Mode: Optional[Literal["简洁", "详细"]] = Field(
|
||||
default=None, description="用户配置模式"
|
||||
)
|
||||
StageMode: Optional[str] = Field(default=None, description="关卡配置模式")
|
||||
Server: Optional[
|
||||
Literal["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"]
|
||||
] = Field(default=None, description="服务器")
|
||||
Status: Optional[bool] = Field(default=None, description="用户状态")
|
||||
RemainedDay: Optional[int] = Field(default=None, description="剩余天数")
|
||||
Annihilation: Optional[
|
||||
Literal[
|
||||
"Close",
|
||||
"Annihilation",
|
||||
"Chernobog@Annihilation",
|
||||
"LungmenOutskirts@Annihilation",
|
||||
"LungmenDowntown@Annihilation",
|
||||
]
|
||||
] = Field(default=None, description="剿灭模式")
|
||||
Routine: Optional[bool] = Field(default=None, description="是否启用日常")
|
||||
InfrastMode: Optional[Literal["Normal", "Rotation", "Custom"]] = Field(
|
||||
default=None, description="基建模式"
|
||||
)
|
||||
InfrastPath: Optional[str] = Field(default=None, description="自定义基建文件路径")
|
||||
Password: Optional[str] = Field(default=None, description="密码")
|
||||
Notes: Optional[str] = Field(default=None, description="备注")
|
||||
MedicineNumb: Optional[int] = Field(default=None, description="吃理智药数量")
|
||||
SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field(
|
||||
default=None, description="连战次数"
|
||||
)
|
||||
Stage: Optional[str] = Field(default=None, description="关卡选择")
|
||||
Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1")
|
||||
Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2")
|
||||
Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3")
|
||||
Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡")
|
||||
IfSkland: Optional[bool] = Field(default=None, description="是否启用森空岛签到")
|
||||
SklandToken: Optional[str] = Field(default=None, description="SklandToken")
|
||||
|
||||
|
||||
class MaaUserConfig_Data(BaseModel):
|
||||
LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期")
|
||||
LastAnnihilationDate: Optional[str] = Field(
|
||||
default=None, description="上次剿灭日期"
|
||||
)
|
||||
LastSklandDate: Optional[str] = Field(
|
||||
default=None, description="上次森空岛签到日期"
|
||||
)
|
||||
ProxyTimes: Optional[int] = Field(default=None, description="代理次数")
|
||||
IfPassCheck: Optional[bool] = Field(default=None, description="是否通过人工排查")
|
||||
|
||||
|
||||
class MaaUserConfig_Task(BaseModel):
|
||||
IfWakeUp: Optional[bool] = Field(default=None, description="开始唤醒")
|
||||
IfRecruiting: Optional[bool] = Field(default=None, description="自动公招")
|
||||
IfBase: Optional[bool] = Field(default=None, description="基建换班")
|
||||
IfCombat: Optional[bool] = Field(default=None, description="刷理智")
|
||||
IfMall: Optional[bool] = Field(default=None, description="获取信用及购物")
|
||||
IfMission: Optional[bool] = Field(default=None, description="领取奖励")
|
||||
IfAutoRoguelike: Optional[bool] = Field(default=None, description="自动肉鸽")
|
||||
IfReclamation: Optional[bool] = Field(default=None, description="生息演算")
|
||||
|
||||
|
||||
class MaaUserConfig_Notify(BaseModel):
|
||||
Enabled: Optional[bool] = Field(default=None, description="是否启用通知")
|
||||
IfSendStatistic: Optional[bool] = Field(
|
||||
default=None, description="是否发送统计信息"
|
||||
)
|
||||
IfSendSixStar: Optional[bool] = Field(default=None, description="是否发送高资喜报")
|
||||
IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知")
|
||||
ToAddress: Optional[str] = Field(default=None, description="邮件接收地址")
|
||||
IfServerChan: Optional[bool] = Field(
|
||||
default=None, description="是否使用Server酱推送"
|
||||
)
|
||||
ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey")
|
||||
CustomWebhooks: Optional[List[CustomWebhook]] = Field(
|
||||
default=None, description="用户自定义Webhook列表"
|
||||
)
|
||||
|
||||
|
||||
class GeneralUserConfig_Notify(BaseModel):
|
||||
Enabled: Optional[bool] = Field(default=None, description="是否启用通知")
|
||||
IfSendStatistic: Optional[bool] = Field(
|
||||
default=None, description="是否发送统计信息"
|
||||
)
|
||||
IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知")
|
||||
ToAddress: Optional[str] = Field(default=None, description="邮件接收地址")
|
||||
IfServerChan: Optional[bool] = Field(
|
||||
default=None, description="是否使用Server酱推送"
|
||||
)
|
||||
ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey")
|
||||
IfCompanyWebHookBot: Optional[bool] = Field(
|
||||
default=None, description="是否使用Webhook推送"
|
||||
)
|
||||
CompanyWebHookBotUrl: Optional[str] = Field(
|
||||
default=None, description="企微Webhook Bot URL"
|
||||
)
|
||||
|
||||
|
||||
class MaaUserConfig(BaseModel):
|
||||
Info: Optional[MaaUserConfig_Info] = Field(default=None, description="基础信息")
|
||||
Data: Optional[MaaUserConfig_Data] = Field(default=None, description="用户数据")
|
||||
Task: Optional[MaaUserConfig_Task] = Field(default=None, description="任务列表")
|
||||
Notify: Optional[MaaUserConfig_Notify] = Field(default=None, description="单独通知")
|
||||
|
||||
|
||||
class MaaConfig_Info(BaseModel):
|
||||
Name: Optional[str] = Field(default=None, description="脚本名称")
|
||||
Path: Optional[str] = Field(default=None, description="脚本路径")
|
||||
|
||||
|
||||
class MaaConfig_Run(BaseModel):
|
||||
TaskTransitionMethod: Optional[Literal["NoAction", "ExitGame", "ExitEmulator"]] = (
|
||||
Field(default=None, description="简洁任务间切换方式")
|
||||
)
|
||||
ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制")
|
||||
ADBSearchRange: Optional[int] = Field(default=None, description="ADB端口搜索范围")
|
||||
RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制")
|
||||
AnnihilationTimeLimit: Optional[int] = Field(
|
||||
default=None, description="剿灭超时限制"
|
||||
)
|
||||
RoutineTimeLimit: Optional[int] = Field(default=None, description="日常超时限制")
|
||||
AnnihilationWeeklyLimit: Optional[bool] = Field(
|
||||
default=None, description="剿灭每周仅代理至上限"
|
||||
)
|
||||
|
||||
|
||||
class MaaConfig(BaseModel):
|
||||
Info: Optional[MaaConfig_Info] = Field(default=None, description="脚本基础信息")
|
||||
Run: Optional[MaaConfig_Run] = Field(default=None, description="脚本运行配置")
|
||||
|
||||
|
||||
class GeneralUserConfig_Info(BaseModel):
|
||||
|
||||
Name: Optional[str] = Field(default=None, description="用户名")
|
||||
Status: Optional[bool] = Field(default=None, description="用户状态")
|
||||
RemainedDay: Optional[int] = Field(default=None, description="剩余天数")
|
||||
IfScriptBeforeTask: Optional[bool] = Field(
|
||||
default=None, description="是否在任务前执行脚本"
|
||||
)
|
||||
ScriptBeforeTask: Optional[str] = Field(default=None, description="任务前脚本路径")
|
||||
IfScriptAfterTask: Optional[bool] = Field(
|
||||
default=None, description="是否在任务后执行脚本"
|
||||
)
|
||||
ScriptAfterTask: Optional[str] = Field(default=None, description="任务后脚本路径")
|
||||
Notes: Optional[str] = Field(default=None, description="备注")
|
||||
|
||||
|
||||
class GeneralUserConfig_Data(BaseModel):
|
||||
LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期")
|
||||
ProxyTimes: Optional[int] = Field(default=None, description="代理次数")
|
||||
|
||||
|
||||
class GeneralUserConfig(BaseModel):
|
||||
Info: Optional[GeneralUserConfig_Info] = Field(default=None, description="用户信息")
|
||||
Data: Optional[GeneralUserConfig_Data] = Field(default=None, description="用户数据")
|
||||
Notify: Optional[GeneralUserConfig_Notify] = Field(
|
||||
default=None, description="单独通知"
|
||||
)
|
||||
|
||||
|
||||
class GeneralConfig_Info(BaseModel):
|
||||
Name: Optional[str] = Field(default=None, description="脚本名称")
|
||||
RootPath: Optional[str] = Field(default=None, description="脚本根目录")
|
||||
|
||||
|
||||
class GeneralConfig_Script(BaseModel):
|
||||
ScriptPath: Optional[str] = Field(default=None, description="脚本可执行文件路径")
|
||||
Arguments: Optional[str] = Field(default=None, description="脚本启动附加命令参数")
|
||||
IfTrackProcess: Optional[bool] = Field(
|
||||
default=None, description="是否追踪脚本子进程"
|
||||
)
|
||||
ConfigPath: Optional[str] = Field(default=None, description="配置文件路径")
|
||||
ConfigPathMode: Optional[Literal["File", "Folder"]] = Field(
|
||||
default=None, description="配置文件类型: 单个文件, 文件夹"
|
||||
)
|
||||
UpdateConfigMode: Optional[Literal["Never", "Success", "Failure", "Always"]] = (
|
||||
Field(
|
||||
default=None,
|
||||
description="更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时",
|
||||
)
|
||||
)
|
||||
LogPath: Optional[str] = Field(default=None, description="日志文件路径")
|
||||
LogPathFormat: Optional[str] = Field(default=None, description="日志文件名格式")
|
||||
LogTimeStart: Optional[int] = Field(default=None, description="日志时间戳开始位置")
|
||||
LogTimeEnd: Optional[int] = Field(default=None, description="日志时间戳结束位置")
|
||||
LogTimeFormat: Optional[str] = Field(default=None, description="日志时间戳格式")
|
||||
SuccessLog: Optional[str] = Field(default=None, description="成功时日志")
|
||||
ErrorLog: Optional[str] = Field(default=None, description="错误时日志")
|
||||
|
||||
|
||||
class GeneralConfig_Game(BaseModel):
|
||||
Enabled: Optional[bool] = Field(
|
||||
default=None, description="游戏/模拟器相关功能是否启用"
|
||||
)
|
||||
Type: Optional[Literal["Emulator", "Client"]] = Field(
|
||||
default=None, description="类型: 模拟器, PC端"
|
||||
)
|
||||
Path: Optional[str] = Field(default=None, description="游戏/模拟器程序路径")
|
||||
Arguments: Optional[str] = Field(default=None, description="游戏/模拟器启动参数")
|
||||
WaitTime: Optional[int] = Field(default=None, description="游戏/模拟器等待启动时间")
|
||||
IfForceClose: Optional[bool] = Field(
|
||||
default=None, description="是否强制关闭游戏/模拟器进程"
|
||||
)
|
||||
|
||||
|
||||
class GeneralConfig_Run(BaseModel):
|
||||
ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制")
|
||||
RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制")
|
||||
RunTimeLimit: Optional[int] = Field(default=None, description="日志超时限制")
|
||||
|
||||
|
||||
class GeneralConfig(BaseModel):
|
||||
|
||||
Info: Optional[GeneralConfig_Info] = Field(default=None, description="脚本基础信息")
|
||||
Script: Optional[GeneralConfig_Script] = Field(default=None, description="脚本配置")
|
||||
Game: Optional[GeneralConfig_Game] = Field(default=None, description="游戏配置")
|
||||
Run: Optional[GeneralConfig_Run] = Field(default=None, description="运行配置")
|
||||
|
||||
|
||||
class PlanIndexItem(BaseModel):
|
||||
uid: str = Field(..., description="唯一标识符")
|
||||
type: Literal["MaaPlanConfig"] = Field(..., description="配置类型")
|
||||
|
||||
|
||||
class MaaPlanConfig_Info(BaseModel):
|
||||
Name: Optional[str] = Field(default=None, description="计划表名称")
|
||||
Mode: Optional[Literal["ALL", "Weekly"]] = Field(
|
||||
default=None, description="计划表模式"
|
||||
)
|
||||
|
||||
|
||||
class MaaPlanConfig_Item(BaseModel):
|
||||
MedicineNumb: Optional[int] = Field(default=None, description="吃理智药")
|
||||
SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field(
|
||||
None, description="连战次数"
|
||||
)
|
||||
Stage: Optional[str] = Field(default=None, description="关卡选择")
|
||||
Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1")
|
||||
Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2")
|
||||
Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3")
|
||||
Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡")
|
||||
|
||||
|
||||
class MaaPlanConfig(BaseModel):
|
||||
|
||||
Info: Optional[MaaPlanConfig_Info] = Field(default=None, description="基础信息")
|
||||
ALL: Optional[MaaPlanConfig_Item] = Field(default=None, description="全局")
|
||||
Monday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周一")
|
||||
Tuesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周二")
|
||||
Wednesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周三")
|
||||
Thursday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周四")
|
||||
Friday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周五")
|
||||
Saturday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周六")
|
||||
Sunday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周日")
|
||||
|
||||
|
||||
class HistoryIndexItem(BaseModel):
|
||||
date: str = Field(..., description="日期")
|
||||
status: Literal["完成", "异常"] = Field(..., description="状态")
|
||||
jsonFile: str = Field(..., description="对应JSON文件")
|
||||
|
||||
|
||||
class HistoryData(BaseModel):
|
||||
index: Optional[List[HistoryIndexItem]] = Field(
|
||||
default=None, description="历史记录索引列表"
|
||||
)
|
||||
recruit_statistics: Optional[Dict[str, int]] = Field(
|
||||
default=None, description="公招统计数据, key为星级, value为对应的公招数量"
|
||||
)
|
||||
drop_statistics: Optional[Dict[str, Dict[str, int]]] = Field(
|
||||
default=None,
|
||||
description="掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } }",
|
||||
)
|
||||
error_info: Optional[Dict[str, str]] = Field(
|
||||
default=None, description="报错信息, key为时间戳, value为错误描述"
|
||||
)
|
||||
log_content: Optional[str] = Field(
|
||||
default=None, description="日志内容, 仅在提取单条历史记录数据时返回"
|
||||
)
|
||||
|
||||
|
||||
class ScriptCreateIn(BaseModel):
|
||||
type: Literal["MAA", "General"] = Field(
|
||||
..., description="脚本类型: MAA脚本, 通用脚本"
|
||||
)
|
||||
|
||||
|
||||
class ScriptCreateOut(OutBase):
|
||||
scriptId: str = Field(..., description="新创建的脚本ID")
|
||||
data: Union[MaaConfig, GeneralConfig] = Field(..., description="脚本配置数据")
|
||||
|
||||
|
||||
class ScriptGetIn(BaseModel):
|
||||
scriptId: Optional[str] = Field(
|
||||
default=None, description="脚本ID, 未携带时表示获取所有脚本数据"
|
||||
)
|
||||
|
||||
|
||||
class ScriptGetOut(OutBase):
|
||||
index: List[ScriptIndexItem] = Field(..., description="脚本索引列表")
|
||||
data: Dict[str, Union[MaaConfig, GeneralConfig]] = Field(
|
||||
..., description="脚本数据字典, key来自于index列表的uid"
|
||||
)
|
||||
|
||||
|
||||
class ScriptUpdateIn(BaseModel):
|
||||
scriptId: str = Field(..., description="脚本ID")
|
||||
data: Union[MaaConfig, GeneralConfig] = Field(..., description="脚本更新数据")
|
||||
|
||||
|
||||
class ScriptDeleteIn(BaseModel):
|
||||
scriptId: str = Field(..., description="脚本ID")
|
||||
|
||||
|
||||
class ScriptReorderIn(BaseModel):
|
||||
indexList: List[str] = Field(..., description="脚本ID列表, 按新顺序排列")
|
||||
|
||||
|
||||
class ScriptFileIn(BaseModel):
|
||||
scriptId: str = Field(..., description="脚本ID")
|
||||
jsonFile: str = Field(..., description="配置文件路径")
|
||||
|
||||
|
||||
class ScriptUrlIn(BaseModel):
|
||||
scriptId: str = Field(..., description="脚本ID")
|
||||
url: str = Field(..., description="配置文件URL")
|
||||
|
||||
|
||||
class ScriptUploadIn(BaseModel):
|
||||
scriptId: str = Field(..., description="脚本ID")
|
||||
config_name: str = Field(..., description="配置名称")
|
||||
author: str = Field(..., description="作者")
|
||||
description: str = Field(..., description="描述")
|
||||
|
||||
|
||||
class UserInBase(BaseModel):
|
||||
scriptId: str = Field(..., description="所属脚本ID")
|
||||
|
||||
|
||||
class UserGetIn(UserInBase):
|
||||
userId: Optional[str] = Field(
|
||||
default=None, description="用户ID, 未携带时表示获取所有用户数据"
|
||||
)
|
||||
|
||||
|
||||
class UserGetOut(OutBase):
|
||||
index: List[UserIndexItem] = Field(..., description="用户索引列表")
|
||||
data: Dict[str, Union[MaaUserConfig, GeneralUserConfig]] = Field(
|
||||
..., description="用户数据字典, key来自于index列表的uid"
|
||||
)
|
||||
|
||||
|
||||
class UserCreateOut(OutBase):
|
||||
userId: str = Field(..., description="新创建的用户ID")
|
||||
data: Union[MaaUserConfig, GeneralUserConfig] = Field(
|
||||
..., description="用户配置数据"
|
||||
)
|
||||
|
||||
|
||||
class UserUpdateIn(UserInBase):
|
||||
userId: str = Field(..., description="用户ID")
|
||||
data: Union[MaaUserConfig, GeneralUserConfig] = Field(
|
||||
..., description="用户更新数据"
|
||||
)
|
||||
|
||||
|
||||
class UserDeleteIn(UserInBase):
|
||||
userId: str = Field(..., description="用户ID")
|
||||
|
||||
|
||||
class UserReorderIn(UserInBase):
|
||||
indexList: List[str] = Field(..., description="用户ID列表, 按新顺序排列")
|
||||
|
||||
|
||||
class UserSetIn(UserInBase):
|
||||
userId: str = Field(..., description="用户ID")
|
||||
jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件")
|
||||
|
||||
|
||||
class PlanCreateIn(BaseModel):
|
||||
type: Literal["MaaPlan"]
|
||||
|
||||
|
||||
class PlanCreateOut(OutBase):
|
||||
planId: str = Field(..., description="新创建的计划ID")
|
||||
data: MaaPlanConfig = Field(..., description="计划配置数据")
|
||||
|
||||
|
||||
class PlanGetIn(BaseModel):
|
||||
planId: Optional[str] = Field(
|
||||
default=None, description="计划ID, 未携带时表示获取所有计划数据"
|
||||
)
|
||||
|
||||
|
||||
class PlanGetOut(OutBase):
|
||||
index: List[PlanIndexItem] = Field(..., description="计划索引列表")
|
||||
data: Dict[str, MaaPlanConfig] = Field(..., description="计划列表或单个计划数据")
|
||||
|
||||
|
||||
class PlanUpdateIn(BaseModel):
|
||||
planId: str = Field(..., description="计划ID")
|
||||
data: MaaPlanConfig = Field(..., description="计划更新数据")
|
||||
|
||||
|
||||
class PlanDeleteIn(BaseModel):
|
||||
planId: str = Field(..., description="计划ID")
|
||||
|
||||
|
||||
class PlanReorderIn(BaseModel):
|
||||
indexList: List[str] = Field(..., description="计划ID列表, 按新顺序排列")
|
||||
|
||||
|
||||
class QueueCreateOut(OutBase):
|
||||
queueId: str = Field(..., description="新创建的队列ID")
|
||||
data: QueueConfig = Field(..., description="队列配置数据")
|
||||
|
||||
|
||||
class QueueGetIn(BaseModel):
|
||||
queueId: Optional[str] = Field(
|
||||
default=None, description="队列ID, 未携带时表示获取所有队列数据"
|
||||
)
|
||||
|
||||
|
||||
class QueueGetOut(OutBase):
|
||||
index: List[QueueIndexItem] = Field(..., description="队列索引列表")
|
||||
data: Dict[str, QueueConfig] = Field(
|
||||
..., description="队列数据字典, key来自于index列表的uid"
|
||||
)
|
||||
|
||||
|
||||
class QueueUpdateIn(BaseModel):
|
||||
queueId: str = Field(..., description="队列ID")
|
||||
data: QueueConfig = Field(..., description="队列更新数据")
|
||||
|
||||
|
||||
class QueueDeleteIn(BaseModel):
|
||||
queueId: str = Field(..., description="队列ID")
|
||||
|
||||
|
||||
class QueueReorderIn(BaseModel):
|
||||
indexList: List[str] = Field(..., description="按新顺序排列的调度队列UID列表")
|
||||
|
||||
|
||||
class QueueSetInBase(BaseModel):
|
||||
queueId: str = Field(..., description="所属队列ID")
|
||||
|
||||
|
||||
class TimeSetGetIn(QueueSetInBase):
|
||||
timeSetId: Optional[str] = Field(
|
||||
default=None, description="时间设置ID, 未携带时表示获取所有时间设置数据"
|
||||
)
|
||||
|
||||
|
||||
class TimeSetGetOut(OutBase):
|
||||
index: List[TimeSetIndexItem] = Field(..., description="时间设置索引列表")
|
||||
data: Dict[str, TimeSet] = Field(
|
||||
..., description="时间设置数据字典, key来自于index列表的uid"
|
||||
)
|
||||
|
||||
|
||||
class TimeSetCreateOut(OutBase):
|
||||
timeSetId: str = Field(..., description="新创建的时间设置ID")
|
||||
data: TimeSet = Field(..., description="时间设置配置数据")
|
||||
|
||||
|
||||
class TimeSetUpdateIn(QueueSetInBase):
|
||||
timeSetId: str = Field(..., description="时间设置ID")
|
||||
data: TimeSet = Field(..., description="时间设置更新数据")
|
||||
|
||||
|
||||
class TimeSetDeleteIn(QueueSetInBase):
|
||||
timeSetId: str = Field(..., description="时间设置ID")
|
||||
|
||||
|
||||
class TimeSetReorderIn(QueueSetInBase):
|
||||
indexList: List[str] = Field(..., description="时间设置ID列表, 按新顺序排列")
|
||||
|
||||
|
||||
class QueueItemGetIn(QueueSetInBase):
|
||||
queueItemId: Optional[str] = Field(
|
||||
default=None, description="队列项ID, 未携带时表示获取所有队列项数据"
|
||||
)
|
||||
|
||||
|
||||
class QueueItemGetOut(OutBase):
|
||||
index: List[QueueItemIndexItem] = Field(..., description="队列项索引列表")
|
||||
data: Dict[str, QueueItem] = Field(
|
||||
..., description="队列项数据字典, key来自于index列表的uid"
|
||||
)
|
||||
|
||||
|
||||
class QueueItemCreateOut(OutBase):
|
||||
queueItemId: str = Field(..., description="新创建的队列项ID")
|
||||
data: QueueItem = Field(..., description="队列项配置数据")
|
||||
|
||||
|
||||
class QueueItemUpdateIn(QueueSetInBase):
|
||||
queueItemId: str = Field(..., description="队列项ID")
|
||||
data: QueueItem = Field(..., description="队列项更新数据")
|
||||
|
||||
|
||||
class QueueItemDeleteIn(QueueSetInBase):
|
||||
queueItemId: str = Field(..., description="队列项ID")
|
||||
|
||||
|
||||
class QueueItemReorderIn(QueueSetInBase):
|
||||
indexList: List[str] = Field(..., description="队列项ID列表, 按新顺序排列")
|
||||
|
||||
|
||||
class DispatchIn(BaseModel):
|
||||
taskId: str = Field(
|
||||
...,
|
||||
description="目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID",
|
||||
)
|
||||
|
||||
|
||||
class TaskCreateIn(DispatchIn):
|
||||
mode: Literal["自动代理", "人工排查", "设置脚本"] = Field(
|
||||
..., description="任务模式"
|
||||
)
|
||||
|
||||
|
||||
class TaskCreateOut(OutBase):
|
||||
websocketId: str = Field(..., description="新创建的任务ID")
|
||||
|
||||
|
||||
class WebSocketMessage(BaseModel):
|
||||
id: str = Field(..., description="消息ID, 为Main时表示消息来自主进程")
|
||||
type: Literal["Update", "Message", "Info", "Signal"] = Field(
|
||||
...,
|
||||
description="消息类型 Update: 更新数据, Message: 请求弹出对话框, Info: 需要在UI显示的消息, Signal: 程序信号",
|
||||
)
|
||||
data: Dict[str, Any] = Field(..., description="消息数据, 具体内容根据type类型而定")
|
||||
|
||||
|
||||
class PowerIn(BaseModel):
|
||||
signal: Literal[
|
||||
"NoAction", "Shutdown", "ShutdownForce", "Hibernate", "Sleep", "KillSelf"
|
||||
] = Field(..., description="电源操作信号")
|
||||
|
||||
|
||||
class HistorySearchIn(BaseModel):
|
||||
mode: Literal["按日合并", "按周合并", "按月合并"] = Field(
|
||||
..., description="合并模式"
|
||||
)
|
||||
start_date: str = Field(..., description="开始日期, 格式YYYY-MM-DD")
|
||||
end_date: str = Field(..., description="结束日期, 格式YYYY-MM-DD")
|
||||
|
||||
|
||||
class HistorySearchOut(OutBase):
|
||||
data: Dict[str, Dict[str, HistoryData]] = Field(
|
||||
...,
|
||||
description="历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } }",
|
||||
)
|
||||
|
||||
|
||||
class HistoryDataGetIn(BaseModel):
|
||||
jsonPath: str = Field(..., description="需要提取数据的历史记录JSON文件")
|
||||
|
||||
|
||||
class HistoryDataGetOut(OutBase):
|
||||
data: HistoryData = Field(..., description="历史记录数据")
|
||||
|
||||
|
||||
class SettingGetOut(OutBase):
|
||||
data: GlobalConfig = Field(..., description="全局设置数据")
|
||||
|
||||
|
||||
class SettingUpdateIn(BaseModel):
|
||||
data: GlobalConfig = Field(..., description="全局设置需要更新的数据")
|
||||
|
||||
|
||||
class UpdateCheckIn(BaseModel):
|
||||
current_version: str = Field(..., description="当前前端版本号")
|
||||
if_force: bool = Field(default=False, description="是否强制拉取更新信息")
|
||||
|
||||
|
||||
class UpdateCheckOut(OutBase):
|
||||
if_need_update: bool = Field(..., description="是否需要更新前端")
|
||||
latest_version: str = Field(..., description="最新前端版本号")
|
||||
update_info: Dict[str, List[str]] = Field(..., description="版本更新信息字典")
|
||||
@@ -1,36 +1,31 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA服务包
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
__version__ = "4.2.0"
|
||||
__version__ = "5.0.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .matomo import Matomo
|
||||
from .notification import Notify
|
||||
from .security import Crypto
|
||||
from .system import System
|
||||
from .update import Updater
|
||||
|
||||
__all__ = ["Notify", "Crypto", "System"]
|
||||
__all__ = ["Matomo", "Notify", "System", "Updater"]
|
||||
|
||||
125
app/services/matomo.py
Normal file
125
app/services/matomo.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import uuid
|
||||
import platform
|
||||
import time
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from app.core import Config
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("信息上报")
|
||||
|
||||
|
||||
class _MatomoHandler:
|
||||
"""Matomo统计上报服务"""
|
||||
|
||||
base_url = "https://statistics.auto-mas.top/matomo.php"
|
||||
site_id = "3"
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.session = None
|
||||
|
||||
async def _get_session(self):
|
||||
"""获取HTTP会话"""
|
||||
|
||||
if self.session is None or self.session.closed:
|
||||
timeout = aiohttp.ClientTimeout(total=10)
|
||||
self.session = aiohttp.ClientSession(timeout=timeout)
|
||||
return self.session
|
||||
|
||||
async def close(self):
|
||||
"""关闭HTTP会话"""
|
||||
if self.session and not self.session.closed:
|
||||
await self.session.close()
|
||||
|
||||
def _build_base_params(self, custom_vars: Optional[Dict[str, Any]] = None):
|
||||
"""构建基础参数"""
|
||||
params = {
|
||||
"idsite": self.site_id,
|
||||
"rec": "1",
|
||||
"action_name": "AUTO-MAS后端",
|
||||
"_id": Config.get("Data", "UID")[:16],
|
||||
"uid": Config.get("Data", "UID"),
|
||||
"rand": str(uuid.uuid4().int)[:10],
|
||||
"apiv": "1",
|
||||
"h": time.strftime("%H"),
|
||||
"m": time.strftime("%M"),
|
||||
"s": time.strftime("%S"),
|
||||
"ua": f"AUTO-MAS/{Config.version()} ({platform.system()} {platform.release()})",
|
||||
}
|
||||
|
||||
# 添加自定义变量
|
||||
if custom_vars is not None:
|
||||
cvar = {}
|
||||
for i, (key, value) in enumerate(custom_vars.items(), 1):
|
||||
if i <= 5:
|
||||
cvar[str(i)] = [str(key), str(value)]
|
||||
if cvar:
|
||||
params["_cvar"] = json.dumps(cvar)
|
||||
|
||||
return params
|
||||
|
||||
async def send_event(
|
||||
self,
|
||||
category: str,
|
||||
action: str,
|
||||
name: Optional[str] = None,
|
||||
value: Optional[float] = None,
|
||||
custom_vars: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""发送事件数据到Matomo
|
||||
|
||||
Args:
|
||||
category: 事件类别,如 "Script", "Config", "User"
|
||||
action: 事件动作,如 "Execute", "Update", "Login"
|
||||
name: 事件名称,如具体的脚本名称
|
||||
value: 事件值,如执行时长、文件大小等数值
|
||||
custom_vars: 自定义变量字典
|
||||
"""
|
||||
try:
|
||||
session = await self._get_session()
|
||||
if session is None:
|
||||
return
|
||||
|
||||
params = self._build_base_params(custom_vars)
|
||||
params.update({"e_c": category, "e_a": action, "e_n": name, "e_v": value})
|
||||
params = {k: v for k, v in params.items() if v is not None}
|
||||
|
||||
async with session.get(self.base_url, params=params) as response:
|
||||
if response.status == 200:
|
||||
logger.debug(f"Matomo事件上报成功: {category}/{action}")
|
||||
else:
|
||||
logger.warning(f"Matomo事件上报失败: {response.status}")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("Matomo事件上报超时")
|
||||
except Exception as e:
|
||||
logger.error(f"Matomo事件上报错误: {e}")
|
||||
|
||||
|
||||
Matomo = _MatomoHandler()
|
||||
@@ -1,198 +1,430 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA通知服务
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
import requests
|
||||
from loguru import logger
|
||||
from plyer import notification
|
||||
|
||||
import re
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
import requests
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr
|
||||
from pathlib import Path
|
||||
|
||||
from serverchan_sdk import sc_send
|
||||
from plyer import notification
|
||||
|
||||
from app.core import Config, MainInfoBar
|
||||
from app.services.security import Crypto
|
||||
from app.core import Config
|
||||
from app.utils import get_logger, ImageUtils
|
||||
|
||||
logger = get_logger("通知服务")
|
||||
|
||||
|
||||
class Notification:
|
||||
|
||||
def push_notification(self, title, message, ticker, t):
|
||||
"""推送系统通知"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
if Config.global_config.get(Config.global_config.notify_IfPushPlyer):
|
||||
async def push_plyer(self, title, message, ticker, t) -> bool:
|
||||
"""
|
||||
推送系统通知
|
||||
|
||||
notification.notify(
|
||||
title=title,
|
||||
message=message,
|
||||
app_name="AUTO_MAA",
|
||||
app_icon=str(Config.app_path / "resources/icons/AUTO_MAA.ico"),
|
||||
timeout=t,
|
||||
ticker=ticker,
|
||||
toast=True,
|
||||
)
|
||||
:param title: 通知标题
|
||||
:param message: 通知内容
|
||||
:param ticker: 通知横幅
|
||||
:param t: 通知持续时间
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if Config.get("Notify", "IfPushPlyer"):
|
||||
|
||||
logger.info(f"推送系统通知: {title}")
|
||||
|
||||
if notification.notify is not None:
|
||||
notification.notify(
|
||||
title=title,
|
||||
message=message,
|
||||
app_name="AUTO-MAS",
|
||||
app_icon=(Path.cwd() / "res/icons/AUTO-MAS.ico").as_posix(),
|
||||
timeout=t,
|
||||
ticker=ticker,
|
||||
toast=True,
|
||||
)
|
||||
else:
|
||||
logger.error("plyer.notification 未正确导入, 无法推送系统通知")
|
||||
|
||||
return True
|
||||
|
||||
def send_mail(self, title, content):
|
||||
"""推送邮件通知"""
|
||||
async def send_mail(self, mode, title, content, to_address) -> None:
|
||||
"""
|
||||
推送邮件通知
|
||||
|
||||
if Config.global_config.get(Config.global_config.notify_IfSendMail):
|
||||
:param mode: 邮件内容模式, 支持 "文本" 和 "网页"
|
||||
:param title: 邮件标题
|
||||
:param content: 邮件内容
|
||||
:param to_address: 收件人地址
|
||||
"""
|
||||
|
||||
if (
|
||||
Config.get("Notify", "SMTPServerAddress") == ""
|
||||
or Config.get("Notify", "AuthorizationCode") == ""
|
||||
or not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
Config.get("Notify", "FromAddress"),
|
||||
)
|
||||
)
|
||||
or not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
to_address,
|
||||
)
|
||||
)
|
||||
):
|
||||
logger.error(
|
||||
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
|
||||
)
|
||||
raise ValueError(
|
||||
"邮件通知的SMTP服务器地址、授权码、发件人地址或收件人地址未正确配置"
|
||||
)
|
||||
|
||||
# 定义邮件正文
|
||||
if mode == "文本":
|
||||
message = MIMEText(content, "plain", "utf-8")
|
||||
elif mode == "网页":
|
||||
message = MIMEMultipart("alternative")
|
||||
message["From"] = formataddr(
|
||||
(
|
||||
Header("AUTO-MAS通知服务", "utf-8").encode(),
|
||||
Config.get("Notify", "FromAddress"),
|
||||
)
|
||||
) # 发件人显示的名字
|
||||
message["To"] = formataddr(
|
||||
(Header("AUTO-MAS用户", "utf-8").encode(), to_address)
|
||||
) # 收件人显示的名字
|
||||
message["Subject"] = str(Header(title, "utf-8"))
|
||||
|
||||
if mode == "网页":
|
||||
message.attach(MIMEText(content, "html", "utf-8"))
|
||||
|
||||
smtpObj = smtplib.SMTP_SSL(Config.get("Notify", "SMTPServerAddress"), 465)
|
||||
smtpObj.login(
|
||||
Config.get("Notify", "FromAddress"),
|
||||
Config.get("Notify", "AuthorizationCode"),
|
||||
)
|
||||
smtpObj.sendmail(
|
||||
Config.get("Notify", "FromAddress"), to_address, message.as_string()
|
||||
)
|
||||
smtpObj.quit()
|
||||
logger.success(f"邮件发送成功: {title}")
|
||||
|
||||
async def ServerChanPush(self, title, content, send_key) -> None:
|
||||
"""
|
||||
使用Server酱推送通知
|
||||
|
||||
:param title: 通知标题
|
||||
:param content: 通知内容
|
||||
:param send_key: Server酱的SendKey
|
||||
"""
|
||||
|
||||
if not send_key:
|
||||
raise ValueError("ServerChan SendKey 不能为空")
|
||||
|
||||
# 构造 URL
|
||||
if send_key.startswith("sctp"):
|
||||
match = re.match(r"^sctp(\d+)t", send_key)
|
||||
if match:
|
||||
url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send"
|
||||
else:
|
||||
raise ValueError("SendKey 格式不正确 (sctp<int>)")
|
||||
else:
|
||||
url = f"https://sctapi.ftqq.com/{send_key}.send"
|
||||
|
||||
# 请求发送
|
||||
params = {"title": title, "desp": content}
|
||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||
|
||||
response = requests.post(
|
||||
url, json=params, headers=headers, timeout=10, proxies=Config.get_proxies()
|
||||
)
|
||||
result = response.json()
|
||||
|
||||
if result.get("code") == 0:
|
||||
logger.success(f"Server酱推送通知成功: {title}")
|
||||
else:
|
||||
raise Exception(f"ServerChan 推送通知失败: {response.text}")
|
||||
|
||||
async def CustomWebhookPush(self, title, content, webhook_config) -> None:
|
||||
"""
|
||||
自定义 Webhook 推送通知
|
||||
|
||||
:param title: 通知标题
|
||||
:param content: 通知内容
|
||||
:param webhook_config: Webhook配置对象
|
||||
"""
|
||||
|
||||
if not webhook_config.get("url"):
|
||||
raise ValueError("Webhook URL 不能为空")
|
||||
|
||||
if not webhook_config.get("enabled", True):
|
||||
logger.info(
|
||||
f"Webhook {webhook_config.get('name', 'Unknown')} 已禁用,跳过推送"
|
||||
)
|
||||
return
|
||||
|
||||
# 解析模板
|
||||
template = webhook_config.get(
|
||||
"template", '{"title": "{title}", "content": "{content}"}'
|
||||
)
|
||||
|
||||
# 替换模板变量
|
||||
try:
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 准备模板变量
|
||||
template_vars = {
|
||||
"title": title,
|
||||
"content": content,
|
||||
"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"date": datetime.now().strftime("%Y-%m-%d"),
|
||||
"time": datetime.now().strftime("%H:%M:%S"),
|
||||
}
|
||||
|
||||
logger.debug(f"原始模板: {template}")
|
||||
logger.debug(f"模板变量: {template_vars}")
|
||||
|
||||
# 先尝试作为JSON模板处理
|
||||
try:
|
||||
# 定义邮件正文
|
||||
message = MIMEText(content, "plain", "utf-8")
|
||||
message["From"] = formataddr(
|
||||
(
|
||||
Header("AUTO_MAA通知服务", "utf-8").encode(),
|
||||
Config.global_config.get(
|
||||
Config.global_config.notify_FromAddress
|
||||
),
|
||||
)
|
||||
) # 发件人显示的名字
|
||||
message["To"] = formataddr(
|
||||
(
|
||||
Header("AUTO_MAA用户", "utf-8").encode(),
|
||||
Config.global_config.get(Config.global_config.notify_ToAddress),
|
||||
)
|
||||
) # 收件人显示的名字
|
||||
message["Subject"] = Header(title, "utf-8")
|
||||
# 解析模板为JSON对象,然后替换其中的变量
|
||||
template_obj = json.loads(template)
|
||||
|
||||
smtpObj = smtplib.SMTP_SSL(
|
||||
Config.global_config.get(
|
||||
Config.global_config.notify_SMTPServerAddress
|
||||
),
|
||||
465,
|
||||
# 递归替换JSON对象中的变量
|
||||
def replace_variables(obj):
|
||||
if isinstance(obj, dict):
|
||||
return {k: replace_variables(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [replace_variables(item) for item in obj]
|
||||
elif isinstance(obj, str):
|
||||
result = obj
|
||||
for key, value in template_vars.items():
|
||||
result = result.replace(f"{{{key}}}", str(value))
|
||||
return result
|
||||
else:
|
||||
return obj
|
||||
|
||||
data = replace_variables(template_obj)
|
||||
logger.debug(f"成功解析JSON模板: {data}")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是有效的JSON,作为字符串模板处理
|
||||
logger.debug("模板不是有效JSON,作为字符串模板处理")
|
||||
formatted_template = template
|
||||
for key, value in template_vars.items():
|
||||
# 转义特殊字符以避免JSON解析错误
|
||||
safe_value = (
|
||||
str(value)
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
)
|
||||
formatted_template = formatted_template.replace(
|
||||
f"{{{key}}}", safe_value
|
||||
)
|
||||
|
||||
# 再次尝试解析为JSON
|
||||
try:
|
||||
data = json.loads(formatted_template)
|
||||
logger.debug(f"字符串模板解析为JSON成功: {data}")
|
||||
except json.JSONDecodeError:
|
||||
# 最终作为纯文本发送
|
||||
data = formatted_template
|
||||
logger.debug(f"作为纯文本发送: {data}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"模板解析失败,使用默认格式: {e}")
|
||||
data = {"title": title, "content": content}
|
||||
|
||||
# 准备请求头
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if webhook_config.get("headers"):
|
||||
headers.update(webhook_config["headers"])
|
||||
|
||||
# 发送请求
|
||||
method = webhook_config.get("method", "POST").upper()
|
||||
|
||||
try:
|
||||
if method == "POST":
|
||||
if isinstance(data, dict):
|
||||
response = requests.post(
|
||||
url=webhook_config["url"],
|
||||
json=data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
else:
|
||||
response = requests.post(
|
||||
url=webhook_config["url"],
|
||||
data=data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
else: # GET
|
||||
params = data if isinstance(data, dict) else {"message": data}
|
||||
response = requests.get(
|
||||
url=webhook_config["url"],
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
smtpObj.login(
|
||||
Config.global_config.get(Config.global_config.notify_FromAddress),
|
||||
Crypto.win_decryptor(
|
||||
Config.global_config.get(
|
||||
Config.global_config.notify_AuthorizationCode
|
||||
|
||||
# 检查响应
|
||||
if response.status_code == 200:
|
||||
logger.success(
|
||||
f"自定义Webhook推送成功: {webhook_config.get('name', 'Unknown')} - {title}"
|
||||
)
|
||||
else:
|
||||
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"自定义Webhook推送失败 ({webhook_config.get('name', 'Unknown')}): {str(e)}"
|
||||
)
|
||||
|
||||
async def WebHookPush(self, title, content, webhook_url) -> None:
|
||||
"""
|
||||
WebHook 推送通知 (兼容旧版企业微信格式)
|
||||
|
||||
:param title: 通知标题
|
||||
:param content: 通知内容
|
||||
:param webhook_url: WebHook地址
|
||||
"""
|
||||
|
||||
if not webhook_url:
|
||||
raise ValueError("WebHook 地址不能为空")
|
||||
|
||||
content = f"{title}\n{content}"
|
||||
data = {"msgtype": "text", "text": {"content": content}}
|
||||
|
||||
response = requests.post(
|
||||
url=webhook_url, json=data, timeout=10, proxies=Config.get_proxies()
|
||||
)
|
||||
info = response.json()
|
||||
|
||||
if info["errcode"] == 0:
|
||||
logger.success(f"WebHook 推送通知成功: {title}")
|
||||
else:
|
||||
raise Exception(f"WebHook 推送通知失败: {response.text}")
|
||||
|
||||
async def CompanyWebHookBotPushImage(
|
||||
self, image_path: Path, webhook_url: str
|
||||
) -> None:
|
||||
"""
|
||||
使用企业微信群机器人推送图片通知
|
||||
|
||||
:param image_path: 图片文件路径
|
||||
:param webhook_url: 企业微信群机器人的WebHook地址
|
||||
"""
|
||||
|
||||
if not webhook_url:
|
||||
raise ValueError("webhook URL 不能为空")
|
||||
|
||||
# 压缩图片
|
||||
ImageUtils.compress_image_if_needed(image_path)
|
||||
|
||||
# 检查图片是否存在
|
||||
if not image_path.exists():
|
||||
raise FileNotFoundError(f"文件未找到: {image_path}")
|
||||
|
||||
# 获取图片base64和md5
|
||||
image_base64 = ImageUtils.get_base64_from_file(str(image_path))
|
||||
image_md5 = ImageUtils.calculate_md5_from_file(str(image_path))
|
||||
|
||||
data = {
|
||||
"msgtype": "image",
|
||||
"image": {"base64": image_base64, "md5": image_md5},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
url=webhook_url, json=data, timeout=10, proxies=Config.get_proxies()
|
||||
)
|
||||
info = response.json()
|
||||
|
||||
if info.get("errcode") == 0:
|
||||
logger.success(f"企业微信群机器人推送图片成功: {image_path.name}")
|
||||
else:
|
||||
raise Exception(f"企业微信群机器人推送图片失败: {response.text}")
|
||||
|
||||
async def send_test_notification(self) -> None:
|
||||
"""发送测试通知到所有已启用的通知渠道"""
|
||||
|
||||
logger.info("发送测试通知到所有已启用的通知渠道")
|
||||
|
||||
# 发送系统通知
|
||||
await self.push_plyer(
|
||||
"测试通知",
|
||||
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
|
||||
"测试通知",
|
||||
3,
|
||||
)
|
||||
|
||||
# 发送邮件通知
|
||||
if Config.get("Notify", "IfSendMail"):
|
||||
await self.send_mail(
|
||||
"文本",
|
||||
"AUTO-MAS测试通知",
|
||||
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
|
||||
Config.get("Notify", "ToAddress"),
|
||||
)
|
||||
|
||||
# 发送Server酱通知
|
||||
if Config.get("Notify", "IfServerChan"):
|
||||
await self.ServerChanPush(
|
||||
"AUTO-MAS测试通知",
|
||||
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
|
||||
Config.get("Notify", "ServerChanKey"),
|
||||
)
|
||||
|
||||
# 发送自定义Webhook通知
|
||||
try:
|
||||
custom_webhooks = Config.get("Notify", "CustomWebhooks")
|
||||
except AttributeError:
|
||||
custom_webhooks = []
|
||||
if custom_webhooks:
|
||||
for webhook in custom_webhooks:
|
||||
if webhook.get("enabled", True):
|
||||
try:
|
||||
await self.CustomWebhookPush(
|
||||
"AUTO-MAS测试通知",
|
||||
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
|
||||
webhook,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"自定义Webhook测试失败 ({webhook.get('name', 'Unknown')}): {e}"
|
||||
)
|
||||
),
|
||||
)
|
||||
smtpObj.sendmail(
|
||||
Config.global_config.get(Config.global_config.notify_FromAddress),
|
||||
Config.global_config.get(Config.global_config.notify_ToAddress),
|
||||
message.as_string(),
|
||||
)
|
||||
smtpObj.quit()
|
||||
logger.success("邮件发送成功")
|
||||
except Exception as e:
|
||||
logger.error(f"发送邮件时出错:\n{e}")
|
||||
MainInfoBar.push_info_bar("error", "发送邮件时出错", f"{e}", -1)
|
||||
|
||||
def ServerChanPush(self, title, content):
|
||||
"""使用Server酱推送通知"""
|
||||
|
||||
if Config.global_config.get(Config.global_config.notify_IfServerChan):
|
||||
send_key = Config.global_config.get(
|
||||
Config.global_config.notify_ServerChanKey
|
||||
)
|
||||
option = {}
|
||||
is_valid = lambda s: s == "" or (
|
||||
s == "|".join(s.split("|")) and (s.count("|") == 0 or all(s.split("|")))
|
||||
)
|
||||
"""
|
||||
is_valid => True, 如果启用的话需要正确设置Tag和Channel。
|
||||
允许空的Tag和Channel即不启用,但不允许例如a||b,|a|b,a|b|,||||
|
||||
"""
|
||||
send_tag = Config.global_config.get(
|
||||
Config.global_config.notify_ServerChanTag
|
||||
)
|
||||
send_channel = Config.global_config.get(
|
||||
Config.global_config.notify_ServerChanChannel
|
||||
)
|
||||
|
||||
if is_valid(send_tag):
|
||||
option["tags"] = send_tag
|
||||
else:
|
||||
option["tags"] = ""
|
||||
logger.warning("请正确设置Auto_MAA中ServerChan的Tag。")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning",
|
||||
"Server酱通知推送异常",
|
||||
"请正确设置Auto_MAA中ServerChan的Tag。",
|
||||
-1,
|
||||
)
|
||||
|
||||
if is_valid(send_channel):
|
||||
option["channel"] = send_channel
|
||||
else:
|
||||
option["channel"] = ""
|
||||
logger.warning("请正确设置Auto_MAA中ServerChan的Channel。")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning",
|
||||
"Server酱通知推送异常",
|
||||
"请正确设置Auto_MAA中ServerChan的Channel。",
|
||||
-1,
|
||||
)
|
||||
|
||||
response = sc_send(send_key, title, content, option)
|
||||
if response["code"] == 0:
|
||||
logger.info("Server酱推送通知成功")
|
||||
return True
|
||||
else:
|
||||
logger.info("Server酱推送通知失败")
|
||||
logger.error(response)
|
||||
MainInfoBar.push_info_bar(
|
||||
"error",
|
||||
"Server酱通知推送失败",
|
||||
f'使用Server酱推送通知时出错:\n{response["data"]['error']}',
|
||||
-1,
|
||||
)
|
||||
return f'使用Server酱推送通知时出错:\n{response["data"]['error']}'
|
||||
|
||||
def CompanyWebHookBotPush(self, title, content):
|
||||
"""使用企业微信群机器人推送通知"""
|
||||
if Config.global_config.get(Config.global_config.notify_IfCompanyWebHookBot):
|
||||
content = f"{title}\n{content}"
|
||||
data = {"msgtype": "text", "text": {"content": content}}
|
||||
response = requests.post(
|
||||
url=Config.global_config.get(
|
||||
Config.global_config.notify_CompanyWebHookBotUrl
|
||||
),
|
||||
json=data,
|
||||
)
|
||||
if response.json()["errcode"] == 0:
|
||||
logger.info("企业微信群机器人推送通知成功")
|
||||
return True
|
||||
else:
|
||||
logger.info("企业微信群机器人推送通知失败")
|
||||
logger.error(response.json())
|
||||
MainInfoBar.push_info_bar(
|
||||
"error",
|
||||
"企业微信群机器人通知推送失败",
|
||||
f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}',
|
||||
-1,
|
||||
)
|
||||
return (
|
||||
f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}'
|
||||
)
|
||||
logger.success("测试通知发送完成")
|
||||
|
||||
|
||||
Notify = Notification()
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA安全服务
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import random
|
||||
import secrets
|
||||
import base64
|
||||
import win32crypt
|
||||
from pathlib import Path
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_OAEP
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from typing import List, Dict, Union
|
||||
|
||||
from app.core import Config
|
||||
|
||||
|
||||
class CryptoHandler:
|
||||
|
||||
def get_PASSWORD(self, PASSWORD: str) -> None:
|
||||
"""配置管理密钥"""
|
||||
|
||||
# 生成目录
|
||||
Config.key_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 生成RSA密钥对
|
||||
key = RSA.generate(2048)
|
||||
public_key_local = key.publickey()
|
||||
private_key = key
|
||||
# 保存RSA公钥
|
||||
(Config.app_path / "data/key/public_key.pem").write_bytes(
|
||||
public_key_local.exportKey()
|
||||
)
|
||||
# 生成密钥转换与校验随机盐
|
||||
PASSWORD_salt = secrets.token_hex(random.randint(32, 1024))
|
||||
(Config.app_path / "data/key/PASSWORDsalt.txt").write_text(
|
||||
PASSWORD_salt,
|
||||
encoding="utf-8",
|
||||
)
|
||||
verify_salt = secrets.token_hex(random.randint(32, 1024))
|
||||
(Config.app_path / "data/key/verifysalt.txt").write_text(
|
||||
verify_salt,
|
||||
encoding="utf-8",
|
||||
)
|
||||
# 将管理密钥转化为AES-256密钥
|
||||
AES_password = hashlib.sha256(
|
||||
(PASSWORD + PASSWORD_salt).encode("utf-8")
|
||||
).digest()
|
||||
# 生成AES-256密钥校验哈希值并保存
|
||||
AES_password_verify = hashlib.sha256(
|
||||
AES_password + verify_salt.encode("utf-8")
|
||||
).digest()
|
||||
(Config.app_path / "data/key/AES_password_verify.bin").write_bytes(
|
||||
AES_password_verify
|
||||
)
|
||||
# AES-256加密RSA私钥并保存密文
|
||||
AES_key = AES.new(AES_password, AES.MODE_ECB)
|
||||
private_key_local = AES_key.encrypt(pad(private_key.exportKey(), 32))
|
||||
(Config.app_path / "data/key/private_key.bin").write_bytes(private_key_local)
|
||||
|
||||
def AUTO_encryptor(self, note: str) -> bytes:
|
||||
"""使用AUTO_MAA的算法加密数据"""
|
||||
|
||||
# 读取RSA公钥
|
||||
public_key_local = RSA.import_key(
|
||||
(Config.app_path / "data/key/public_key.pem").read_bytes()
|
||||
)
|
||||
# 使用RSA公钥对数据进行加密
|
||||
cipher = PKCS1_OAEP.new(public_key_local)
|
||||
encrypted = cipher.encrypt(note.encode("utf-8"))
|
||||
return encrypted
|
||||
|
||||
def AUTO_decryptor(self, note: bytes, PASSWORD: str) -> str:
|
||||
"""使用AUTO_MAA的算法解密数据"""
|
||||
|
||||
# 读入RSA私钥密文、盐与校验哈希值
|
||||
private_key_local = (
|
||||
(Config.app_path / "data/key/private_key.bin").read_bytes().strip()
|
||||
)
|
||||
PASSWORD_salt = (
|
||||
(Config.app_path / "data/key/PASSWORDsalt.txt")
|
||||
.read_text(encoding="utf-8")
|
||||
.strip()
|
||||
)
|
||||
verify_salt = (
|
||||
(Config.app_path / "data/key/verifysalt.txt")
|
||||
.read_text(encoding="utf-8")
|
||||
.strip()
|
||||
)
|
||||
AES_password_verify = (
|
||||
(Config.app_path / "data/key/AES_password_verify.bin").read_bytes().strip()
|
||||
)
|
||||
# 将管理密钥转化为AES-256密钥并验证
|
||||
AES_password = hashlib.sha256(
|
||||
(PASSWORD + PASSWORD_salt).encode("utf-8")
|
||||
).digest()
|
||||
AES_password_SHA = hashlib.sha256(
|
||||
AES_password + verify_salt.encode("utf-8")
|
||||
).digest()
|
||||
if AES_password_SHA != AES_password_verify:
|
||||
return "管理密钥错误"
|
||||
else:
|
||||
# AES解密RSA私钥
|
||||
AES_key = AES.new(AES_password, AES.MODE_ECB)
|
||||
private_key_pem = unpad(AES_key.decrypt(private_key_local), 32)
|
||||
private_key = RSA.import_key(private_key_pem)
|
||||
# 使用RSA私钥解密数据
|
||||
decrypter = PKCS1_OAEP.new(private_key)
|
||||
note = decrypter.decrypt(note)
|
||||
return note.decode("utf-8")
|
||||
|
||||
def change_PASSWORD(self, PASSWORD_old: str, PASSWORD_new: str) -> None:
|
||||
"""修改管理密钥"""
|
||||
|
||||
member_list = self.search_member()
|
||||
|
||||
for user_data in member_list:
|
||||
|
||||
# 读取用户数据
|
||||
db = sqlite3.connect(user_data["Path"])
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT * FROM adminx WHERE True")
|
||||
data = cur.fetchall()
|
||||
|
||||
# 使用旧管理密钥解密
|
||||
user_data["Password"] = []
|
||||
for i in range(len(data)):
|
||||
user_data["Password"].append(
|
||||
self.AUTO_decryptor(data[i][12], PASSWORD_old)
|
||||
)
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
self.get_PASSWORD(PASSWORD_new)
|
||||
|
||||
for user_data in member_list:
|
||||
|
||||
# 读取用户数据
|
||||
db = sqlite3.connect(user_data["Path"])
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT * FROM adminx WHERE True")
|
||||
data = cur.fetchall()
|
||||
|
||||
# 使用新管理密钥重新加密
|
||||
for i in range(len(data)):
|
||||
cur.execute(
|
||||
"UPDATE adminx SET password = ? WHERE mode = ? AND uid = ?",
|
||||
(
|
||||
self.AUTO_encryptor(user_data["Password"][i]),
|
||||
data[i][15],
|
||||
data[i][16],
|
||||
),
|
||||
)
|
||||
db.commit()
|
||||
user_data["Password"][i] = None
|
||||
del user_data["Password"]
|
||||
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
def win_encryptor(
|
||||
self, note: str, description: str = None, entropy: bytes = None
|
||||
) -> str:
|
||||
"""使用Windows DPAPI加密数据"""
|
||||
|
||||
encrypted = win32crypt.CryptProtectData(
|
||||
note.encode("utf-8"), description, entropy, None, None, 0
|
||||
)
|
||||
return base64.b64encode(encrypted).decode("utf-8")
|
||||
|
||||
def win_decryptor(self, note: str, entropy: bytes = None) -> str:
|
||||
"""使用Windows DPAPI解密数据"""
|
||||
|
||||
if note == "":
|
||||
return ""
|
||||
|
||||
decrypted = win32crypt.CryptUnprotectData(
|
||||
base64.b64decode(note), entropy, None, None, 0
|
||||
)
|
||||
return decrypted[1].decode("utf-8")
|
||||
|
||||
def search_member(self) -> List[Dict[str, Union[Path, list]]]:
|
||||
"""搜索所有脚本实例及其用户数据库路径"""
|
||||
|
||||
member_list = []
|
||||
|
||||
if (Config.app_path / "config/MaaConfig").exists():
|
||||
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
|
||||
if subdir.is_dir():
|
||||
|
||||
member_list.append({"Path": subdir / "user_data.db"})
|
||||
|
||||
return member_list
|
||||
|
||||
def check_PASSWORD(self, PASSWORD: str) -> bool:
|
||||
"""验证管理密钥"""
|
||||
|
||||
return bool(
|
||||
self.AUTO_decryptor(self.AUTO_encryptor(""), PASSWORD) != "管理密钥错误"
|
||||
)
|
||||
|
||||
|
||||
Crypto = CryptoHandler()
|
||||
@@ -1,60 +1,58 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA系统服务
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import QWidget
|
||||
import sys
|
||||
import ctypes
|
||||
import asyncio
|
||||
import win32gui
|
||||
import win32process
|
||||
import winreg
|
||||
import psutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import getpass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional
|
||||
|
||||
from app.core import Config
|
||||
from app.models.schema import WebSocketMessage
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("系统服务")
|
||||
|
||||
|
||||
class _SystemHandler:
|
||||
|
||||
ES_CONTINUOUS = 0x80000000
|
||||
ES_SYSTEM_REQUIRED = 0x00000001
|
||||
countdown = 60
|
||||
|
||||
def __init__(self, main_window: QWidget = None):
|
||||
def __init__(self) -> None:
|
||||
self.power_task: Optional[asyncio.Task] = None
|
||||
|
||||
self.main_window = main_window
|
||||
|
||||
self.set_Sleep()
|
||||
self.set_SelfStart()
|
||||
|
||||
def set_Sleep(self) -> None:
|
||||
async def set_Sleep(self) -> None:
|
||||
"""同步系统休眠状态"""
|
||||
|
||||
if Config.global_config.get(Config.global_config.function_IfAllowSleep):
|
||||
if Config.get("Function", "IfAllowSleep"):
|
||||
# 设置系统电源状态
|
||||
ctypes.windll.kernel32.SetThreadExecutionState(
|
||||
self.ES_CONTINUOUS | self.ES_SYSTEM_REQUIRED
|
||||
@@ -63,47 +61,154 @@ class _SystemHandler:
|
||||
# 恢复系统电源状态
|
||||
ctypes.windll.kernel32.SetThreadExecutionState(self.ES_CONTINUOUS)
|
||||
|
||||
def set_SelfStart(self) -> None:
|
||||
async def set_SelfStart(self) -> None:
|
||||
"""同步开机自启"""
|
||||
|
||||
if (
|
||||
Config.global_config.get(Config.global_config.start_IfSelfStart)
|
||||
and not self.is_startup()
|
||||
):
|
||||
key = winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
||||
winreg.KEY_SET_VALUE,
|
||||
winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY,
|
||||
)
|
||||
winreg.SetValueEx(key, "AUTO_MAA", 0, winreg.REG_SZ, Config.app_path_sys)
|
||||
winreg.CloseKey(key)
|
||||
elif (
|
||||
not Config.global_config.get(Config.global_config.start_IfSelfStart)
|
||||
and self.is_startup()
|
||||
):
|
||||
key = winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
||||
winreg.KEY_SET_VALUE,
|
||||
winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY,
|
||||
)
|
||||
winreg.DeleteValue(key, "AUTO_MAA")
|
||||
winreg.CloseKey(key)
|
||||
if Config.get("Start", "IfSelfStart") and not await self.is_startup():
|
||||
|
||||
def set_power(self, mode) -> None:
|
||||
# 创建任务计划
|
||||
try:
|
||||
|
||||
# 获取当前用户和时间
|
||||
current_user = getpass.getuser()
|
||||
current_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
# XML 模板
|
||||
xml_content = f"""<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Date>{current_time}</Date>
|
||||
<Author>{current_user}</Author>
|
||||
<Description>AUTO-MAS自启动服务</Description>
|
||||
<URI>\\AUTO-MAS_AutoStart</URI>
|
||||
</RegistrationInfo>
|
||||
<Triggers>
|
||||
<LogonTrigger>
|
||||
<StartBoundary>{current_time}</StartBoundary>
|
||||
<Enabled>true</Enabled>
|
||||
</LogonTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<LogonType>InteractiveToken</LogonType>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>false</AllowHardTerminate>
|
||||
<StartWhenAvailable>true</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>false</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>"{Path.cwd() / 'AUTO-MAS.exe'}"</Command>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>"""
|
||||
|
||||
# 创建临时 XML 文件并执行
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".xml", delete=False, encoding="utf-16"
|
||||
) as f:
|
||||
f.write(xml_content)
|
||||
xml_file = f.name
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"schtasks",
|
||||
"/create",
|
||||
"/tn",
|
||||
"AUTO-MAS_AutoStart",
|
||||
"/xml",
|
||||
xml_file,
|
||||
"/f",
|
||||
],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.success(
|
||||
f"程序自启动任务计划已创建: {Path.cwd() / 'AUTO-MAS.exe'}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"程序自启动任务计划创建失败: {result.stderr}")
|
||||
|
||||
finally:
|
||||
# 删除临时文件
|
||||
try:
|
||||
Path(xml_file).unlink()
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"程序自启动任务计划创建失败: {e}")
|
||||
|
||||
elif not Config.get("Start", "IfSelfStart") and await self.is_startup():
|
||||
|
||||
try:
|
||||
|
||||
result = subprocess.run(
|
||||
["schtasks", "/delete", "/tn", "AUTO-MAS_AutoStart", "/f"],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.success("程序自启动任务计划已删除")
|
||||
else:
|
||||
logger.error(f"程序自启动任务计划删除失败: {result.stderr}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"程序自启动任务计划删除失败: {e}")
|
||||
|
||||
async def set_power(
|
||||
self,
|
||||
mode: Literal[
|
||||
"NoAction", "Shutdown", "ShutdownForce", "Hibernate", "Sleep", "KillSelf"
|
||||
],
|
||||
) -> None:
|
||||
"""
|
||||
执行系统电源操作
|
||||
|
||||
:param mode: 电源操作
|
||||
"""
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
|
||||
if mode == "None":
|
||||
if mode == "NoAction":
|
||||
|
||||
logger.info("不执行系统电源操作")
|
||||
|
||||
elif mode == "Shutdown":
|
||||
|
||||
await self.kill_emulator_processes()
|
||||
logger.info("执行关机操作")
|
||||
subprocess.run(["shutdown", "/s", "/t", "0"])
|
||||
|
||||
elif mode == "ShutdownForce":
|
||||
logger.info("执行强制关机操作")
|
||||
subprocess.run(["shutdown", "/s", "/t", "0", "/f"])
|
||||
|
||||
elif mode == "Hibernate":
|
||||
|
||||
logger.info("执行休眠操作")
|
||||
@@ -116,13 +221,14 @@ class _SystemHandler:
|
||||
["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"]
|
||||
)
|
||||
|
||||
elif mode == "KillSelf":
|
||||
elif mode == "KillSelf" and Config.server is not None:
|
||||
|
||||
self.main_window.close()
|
||||
logger.info("执行退出主程序操作")
|
||||
Config.server.should_exit = True
|
||||
|
||||
elif sys.platform.startswith("linux"):
|
||||
|
||||
if mode == "None":
|
||||
if mode == "NoAction":
|
||||
|
||||
logger.info("不执行系统电源操作")
|
||||
|
||||
@@ -141,30 +247,87 @@ class _SystemHandler:
|
||||
logger.info("执行睡眠操作")
|
||||
subprocess.run(["systemctl", "suspend"])
|
||||
|
||||
elif mode == "KillSelf":
|
||||
elif mode == "KillSelf" and Config.server is not None:
|
||||
|
||||
self.main_window.close()
|
||||
logger.info("执行退出主程序操作")
|
||||
Config.server.should_exit = True
|
||||
|
||||
def is_startup(self) -> bool:
|
||||
async def _power_task(
|
||||
self,
|
||||
power_sign: Literal[
|
||||
"NoAction", "Shutdown", "ShutdownForce", "Hibernate", "Sleep", "KillSelf"
|
||||
],
|
||||
) -> None:
|
||||
"""电源任务"""
|
||||
|
||||
await asyncio.sleep(self.countdown)
|
||||
if power_sign == "KillSelf":
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Main", type="Signal", data={"RequestClose": "请求前端关闭"}
|
||||
).model_dump()
|
||||
)
|
||||
await self.set_power(power_sign)
|
||||
|
||||
async def start_power_task(self):
|
||||
"""开始电源任务"""
|
||||
|
||||
if self.power_task is None or self.power_task.done():
|
||||
self.power_task = asyncio.create_task(self._power_task(Config.power_sign))
|
||||
logger.info(
|
||||
f"电源任务已启动, {self.countdown}秒后执行: {Config.power_sign}"
|
||||
)
|
||||
else:
|
||||
logger.warning("已有电源任务在运行, 请勿重复启动")
|
||||
|
||||
async def cancel_power_task(self):
|
||||
"""取消电源任务"""
|
||||
|
||||
if self.power_task is not None and not self.power_task.done():
|
||||
self.power_task.cancel()
|
||||
try:
|
||||
await self.power_task
|
||||
except asyncio.CancelledError:
|
||||
logger.info("电源任务已取消")
|
||||
else:
|
||||
logger.warning("当前无电源任务在运行")
|
||||
raise RuntimeError("当前无电源任务在运行")
|
||||
|
||||
async def kill_emulator_processes(self):
|
||||
"""这里暂时仅支持 MuMu 模拟器"""
|
||||
|
||||
logger.info("正在清除模拟器进程")
|
||||
|
||||
keywords = ["Nemu", "nemu", "emulator", "MuMu"]
|
||||
for proc in psutil.process_iter(["pid", "name"]):
|
||||
try:
|
||||
pname = proc.info["name"].lower()
|
||||
if any(keyword.lower() in pname for keyword in keywords):
|
||||
proc.kill()
|
||||
logger.info(f"已关闭 MuMu 模拟器进程: {proc.info['name']}")
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
logger.success("模拟器进程清除完成")
|
||||
|
||||
async def is_startup(self) -> bool:
|
||||
"""判断程序是否已经开机自启"""
|
||||
|
||||
key = winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
||||
0,
|
||||
winreg.KEY_READ,
|
||||
)
|
||||
|
||||
try:
|
||||
value, _ = winreg.QueryValueEx(key, "AUTO_MAA")
|
||||
winreg.CloseKey(key)
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
winreg.CloseKey(key)
|
||||
result = subprocess.run(
|
||||
["schtasks", "/query", "/tn", "AUTO-MAS_AutoStart"],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
logger.exception(f"检查任务计划程序失败: {e}")
|
||||
return False
|
||||
|
||||
def get_window_info(self) -> list:
|
||||
"""获取当前窗口信息"""
|
||||
async def get_window_info(self) -> list:
|
||||
"""获取当前前台窗口信息"""
|
||||
|
||||
def callback(hwnd, window_info):
|
||||
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
|
||||
@@ -177,10 +340,16 @@ class _SystemHandler:
|
||||
win32gui.EnumWindows(callback, window_info)
|
||||
return window_info
|
||||
|
||||
def kill_process(self, path: Path) -> None:
|
||||
"""根据路径中止进程"""
|
||||
async def kill_process(self, path: Path) -> None:
|
||||
"""
|
||||
根据路径中止进程
|
||||
|
||||
for pid in self.search_pids(path):
|
||||
:param path: 进程路径
|
||||
"""
|
||||
|
||||
logger.info(f"开始中止进程: {path}")
|
||||
|
||||
for pid in await self.search_pids(path):
|
||||
killprocess = subprocess.Popen(
|
||||
f"taskkill /F /T /PID {pid}",
|
||||
shell=True,
|
||||
@@ -188,8 +357,17 @@ class _SystemHandler:
|
||||
)
|
||||
killprocess.wait()
|
||||
|
||||
def search_pids(self, path: Path) -> list:
|
||||
"""根据路径查找进程PID"""
|
||||
logger.success(f"进程已中止: {path}")
|
||||
|
||||
async def search_pids(self, path: Path) -> list:
|
||||
"""
|
||||
根据路径查找进程PID
|
||||
|
||||
:param path: 进程路径
|
||||
:return: 匹配的进程PID列表
|
||||
"""
|
||||
|
||||
logger.info(f"开始查找进程 PID: {path}")
|
||||
|
||||
pids = []
|
||||
for proc in psutil.process_iter(["pid", "exe"]):
|
||||
@@ -197,7 +375,7 @@ class _SystemHandler:
|
||||
if proc.info["exe"] and proc.info["exe"].lower() == str(path).lower():
|
||||
pids.append(proc.info["pid"])
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
# 进程可能在此期间已结束或无法访问,忽略这些异常
|
||||
# 进程可能在此期间已结束或无法访问, 忽略这些异常
|
||||
pass
|
||||
return pids
|
||||
|
||||
|
||||
389
app/services/update.py
Normal file
389
app/services/update.py
Normal file
@@ -0,0 +1,389 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
import zipfile
|
||||
import requests
|
||||
import subprocess
|
||||
from packaging import version
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
from pathlib import Path
|
||||
|
||||
from app.core import Config
|
||||
from app.models.schema import WebSocketMessage
|
||||
from app.utils.constants import MIRROR_ERROR_INFO
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("更新服务")
|
||||
|
||||
|
||||
class _UpdateHandler:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.is_locked: bool = False
|
||||
self.remote_version: Optional[str] = None
|
||||
self.last_check_time: Optional[datetime] = None
|
||||
self.update_version_info: Optional[Dict[str, List[str]]] = None
|
||||
self.mirror_chyan_download_url: Optional[str] = None
|
||||
|
||||
async def check_update(
|
||||
self, current_version: str, if_force: bool = False
|
||||
) -> tuple[bool, str, Dict[str, List[str]]]:
|
||||
|
||||
if (
|
||||
not if_force
|
||||
and self.remote_version is not None
|
||||
and self.last_check_time is not None
|
||||
and self.update_version_info is not None
|
||||
and self.last_check_time > datetime.now() - timedelta(hours=4)
|
||||
):
|
||||
logger.info("四小时内已进行过一次检查, 直接使用缓存的版本更新信息")
|
||||
return (
|
||||
bool(
|
||||
version.parse(self.remote_version) > version.parse(current_version)
|
||||
),
|
||||
self.remote_version,
|
||||
self.update_version_info,
|
||||
)
|
||||
|
||||
logger.info("开始检查更新")
|
||||
|
||||
response = requests.get(
|
||||
f"https://mirrorchyan.com/api/resources/AUTO_MAA/latest?user_agent=AutoMasGui¤t_version={current_version}&cdk={Config.get('Update', 'MirrorChyanCDK')}&channel=stable",
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
if response.status_code == 200:
|
||||
version_info = response.json()
|
||||
else:
|
||||
result = response.json()
|
||||
|
||||
if result["code"] != 0:
|
||||
if result["code"] in MIRROR_ERROR_INFO:
|
||||
raise Exception(
|
||||
f"获取版本信息时出错: {MIRROR_ERROR_INFO[result['code']]}"
|
||||
)
|
||||
else:
|
||||
raise Exception(
|
||||
"获取版本信息时出错: 意料之外的错误, 请及时联系项目组以获取来自 Mirror 酱的技术支持"
|
||||
)
|
||||
|
||||
logger.success("获取版本信息成功")
|
||||
self.last_check_time = datetime.now()
|
||||
|
||||
self.remote_version = version_info["data"]["version_name"]
|
||||
if self.remote_version is None:
|
||||
raise Exception("Mirror 酱未返回版本号, 请稍后重试")
|
||||
if "url" in version_info["data"]:
|
||||
self.mirror_chyan_download_url = version_info["data"]["url"]
|
||||
|
||||
if version.parse(self.remote_version) > version.parse(current_version):
|
||||
|
||||
# 版本更新信息
|
||||
version_info_json: Dict[str, Dict[str, List[str]]] = json.loads(
|
||||
re.sub(
|
||||
r"^<!--\s*(.*?)\s*-->$",
|
||||
r"\1",
|
||||
version_info["data"]["release_note"].splitlines()[0],
|
||||
)
|
||||
)
|
||||
|
||||
self.update_version_info = {}
|
||||
for v_i in [
|
||||
info
|
||||
for ver, info in version_info_json.items()
|
||||
if version.parse(ver) > version.parse(current_version)
|
||||
]:
|
||||
|
||||
for key, value in v_i.items():
|
||||
if key not in self.update_version_info:
|
||||
self.update_version_info[key] = []
|
||||
self.update_version_info[key] += value
|
||||
|
||||
return True, self.remote_version, self.update_version_info
|
||||
|
||||
else:
|
||||
return False, current_version, {}
|
||||
|
||||
async def download_update(self) -> None:
|
||||
|
||||
if self.is_locked:
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Signal",
|
||||
data={"Failed": "已有更新任务在进行中, 请勿重复操作"},
|
||||
).model_dump()
|
||||
)
|
||||
return None
|
||||
|
||||
self.is_locked = True
|
||||
|
||||
if self.remote_version is None:
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Signal",
|
||||
data={"Failed": "未检测到可用的远程版本, 请先检查更新"},
|
||||
).model_dump()
|
||||
)
|
||||
self.is_locked = False
|
||||
return None
|
||||
|
||||
if (Path.cwd() / f"UpdatePack_{self.remote_version}.zip").exists():
|
||||
logger.info(
|
||||
f"更新包已存在: {Path.cwd() / f'UpdatePack_{self.remote_version}.zip'}"
|
||||
)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Signal",
|
||||
data={
|
||||
"Accomplish": str(
|
||||
Path.cwd() / f"UpdatePack_{self.remote_version}.zip"
|
||||
)
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
self.is_locked = False
|
||||
return None
|
||||
|
||||
if Config.get("Update", "Source") == "GitHub":
|
||||
|
||||
download_url = f"https://github.com/AUTO-MAS-Project/AUTO-MAS/releases/download/{self.remote_version}/AUTO-MAS_{self.remote_version}.zip"
|
||||
|
||||
elif Config.get("Update", "Source") == "MirrorChyan":
|
||||
|
||||
if self.mirror_chyan_download_url is None:
|
||||
logger.warning("MirrorChyan 未返回下载链接, 使用自建下载站")
|
||||
download_url = f"https://download.auto-mas.top/d/AUTO-MAS/AUTO-MAS_{self.remote_version}.zip"
|
||||
|
||||
else:
|
||||
with requests.get(
|
||||
self.mirror_chyan_download_url,
|
||||
allow_redirects=True,
|
||||
timeout=10,
|
||||
stream=True,
|
||||
proxies=Config.get_proxies(),
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
download_url = response.url
|
||||
elif Config.get("Update", "Source") == "AutoSite":
|
||||
download_url = f"https://download.auto-mas.top/d/AUTO-MAS/AUTO-MAS_{self.remote_version}.zip"
|
||||
|
||||
else:
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Signal",
|
||||
data={
|
||||
"Failed": f"未知的下载源: {Config.get('Update', 'Source')}, 请检查配置文件"
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
self.is_locked = False
|
||||
return None
|
||||
|
||||
logger.info(f"开始下载: {download_url}")
|
||||
|
||||
check_times = 3
|
||||
while check_times != 0:
|
||||
|
||||
try:
|
||||
# 清理可能存在的临时文件
|
||||
if (Path.cwd() / "download.temp").exists():
|
||||
(Path.cwd() / "download.temp").unlink()
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
response = requests.get(
|
||||
download_url, timeout=10, stream=True, proxies=Config.get_proxies()
|
||||
)
|
||||
|
||||
if response.status_code not in [200, 206]:
|
||||
|
||||
if check_times != -1:
|
||||
check_times -= 1
|
||||
|
||||
logger.warning(
|
||||
f"连接失败: {download_url}, 状态码: {response.status_code}, 剩余重试次数: {check_times}"
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
logger.info(f"连接成功: {download_url}, 状态码: {response.status_code}")
|
||||
|
||||
file_size = int(response.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
last_download_size = 0
|
||||
speed = 0
|
||||
last_time = time.time()
|
||||
with (Path.cwd() / "download.temp").open(mode="wb") as f:
|
||||
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# 更新指定线程的下载进度, 每秒更新一次
|
||||
if time.time() - last_time >= 1.0:
|
||||
speed = (
|
||||
(downloaded_size - last_download_size)
|
||||
/ (time.time() - last_time)
|
||||
/ 1024
|
||||
)
|
||||
last_download_size = downloaded_size
|
||||
last_time = time.time()
|
||||
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Update",
|
||||
data={
|
||||
"downloaded_size": downloaded_size,
|
||||
"file_size": file_size,
|
||||
"speed": speed,
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
(Path.cwd() / "download.temp").rename(
|
||||
Path.cwd() / f"UpdatePack_{self.remote_version}.zip"
|
||||
)
|
||||
|
||||
logger.success(
|
||||
f"下载完成: {download_url}, 实际下载大小: {downloaded_size} 字节, 耗时: {time.time() - start_time:.2f} 秒, 保存位置: {Path.cwd() / f'UpdatePack_{self.remote_version}.zip'}"
|
||||
)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Signal",
|
||||
data={
|
||||
"Accomplish": str(
|
||||
Path.cwd() / f"UpdatePack_{self.remote_version}.zip"
|
||||
)
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
self.is_locked = False
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
|
||||
if check_times != -1:
|
||||
check_times -= 1
|
||||
|
||||
logger.info(
|
||||
f"下载出错: {download_url}, 错误信息: {e}, 剩余重试次数: {check_times}"
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
else:
|
||||
|
||||
if (Path.cwd() / "download.temp").exists():
|
||||
(Path.cwd() / "download.temp").unlink()
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Signal",
|
||||
data={"Failed": f"下载失败: {download_url}"},
|
||||
).model_dump()
|
||||
)
|
||||
self.is_locked = False
|
||||
|
||||
async def install_update(self):
|
||||
|
||||
if self.is_locked:
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Signal",
|
||||
data={"Failed": "已有更新任务在进行中, 请勿重复操作"},
|
||||
).model_dump()
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info("开始应用更新")
|
||||
self.is_locked = True
|
||||
|
||||
versions = {
|
||||
version.parse(match.group(1)): f.name
|
||||
for f in Path.cwd().glob("UpdatePack_*.zip")
|
||||
if (match := re.match(r"UpdatePack_(.+)\.zip$", f.name))
|
||||
}
|
||||
logger.info(f"检测到的更新包: {versions.values()}")
|
||||
|
||||
if not versions:
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Signal",
|
||||
data={"Failed": "未检测到更新包, 请先下载更新"},
|
||||
).model_dump()
|
||||
)
|
||||
self.is_locked = False
|
||||
return None
|
||||
|
||||
update_package = Path.cwd() / versions[max(versions)]
|
||||
|
||||
logger.info(f"开始解压: {update_package} 到 {Path.cwd()}")
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(update_package, "r") as zip_ref:
|
||||
zip_ref.extractall(Path.cwd())
|
||||
except Exception as e:
|
||||
logger.error(f"解压失败, {type(e).__name__}: {e}")
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Info",
|
||||
data={"Error": f"解压失败, {type(e).__name__}: {e}"},
|
||||
).model_dump()
|
||||
)
|
||||
self.is_locked = False
|
||||
return None
|
||||
|
||||
logger.success(f"解压完成: {update_package} 到 {Path.cwd()}")
|
||||
|
||||
logger.info("正在删除临时文件与旧更新包文件")
|
||||
if (Path.cwd() / "changes.json").exists():
|
||||
(Path.cwd() / "changes.json").unlink()
|
||||
for f in versions.values():
|
||||
if (Path.cwd() / f).exists():
|
||||
(Path.cwd() / f).unlink()
|
||||
|
||||
logger.info("启动更新程序")
|
||||
self.is_locked = False
|
||||
subprocess.Popen(
|
||||
[Path.cwd() / "AUTO-MAS-Setup.exe"],
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.DETACHED_PROCESS
|
||||
| subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
|
||||
|
||||
Updater = _UpdateHandler()
|
||||
2132
app/task/MAA.py
Normal file
2132
app/task/MAA.py
Normal file
File diff suppressed because it is too large
Load Diff
32
app/task/__init__.py
Normal file
32
app/task/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
__version__ = "5.0.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
|
||||
from .skland import skland_sign_in
|
||||
from .general import GeneralManager
|
||||
from .MAA import MaaManager
|
||||
|
||||
__all__ = ["skland_sign_in", "GeneralManager", "MaaManager"]
|
||||
1099
app/task/general.py
Normal file
1099
app/task/general.py
Normal file
File diff suppressed because it is too large
Load Diff
266
app/task/skland.py
Normal file
266
app/task/skland.py
Normal file
@@ -0,0 +1,266 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 ClozyA
|
||||
|
||||
# This file incorporates work covered by the following copyright and
|
||||
# permission notice:
|
||||
#
|
||||
# skland-checkin-ghaction Copyright © 2023 Yanstory
|
||||
# https://github.com/Yanstory/skland-checkin-ghaction
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import time
|
||||
import json
|
||||
import hmac
|
||||
import asyncio
|
||||
import hashlib
|
||||
import requests
|
||||
from urllib import parse
|
||||
|
||||
from app.core import Config
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("森空岛签到任务")
|
||||
|
||||
|
||||
async def skland_sign_in(token) -> dict:
|
||||
"""森空岛签到"""
|
||||
|
||||
app_code = "4ca99fa6b56cc2ba"
|
||||
# 用于获取grant code
|
||||
grant_code_url = "https://as.hypergryph.com/user/oauth2/v2/grant"
|
||||
# 用于获取cred
|
||||
cred_code_url = "https://zonai.skland.com/api/v1/user/auth/generate_cred_by_code"
|
||||
# 查询角色绑定
|
||||
binding_url = "https://zonai.skland.com/api/v1/game/player/binding"
|
||||
# 签到接口
|
||||
sign_url = "https://zonai.skland.com/api/v1/game/attendance"
|
||||
|
||||
# 基础请求头
|
||||
header = {
|
||||
"cred": "",
|
||||
"User-Agent": "Skland/1.5.1 (com.hypergryph.skland; build:100501001; Android 34;) Okhttp/4.11.0",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Connection": "close",
|
||||
}
|
||||
header_login = header.copy()
|
||||
header_for_sign = {
|
||||
"platform": "1",
|
||||
"timestamp": "",
|
||||
"dId": "",
|
||||
"vName": "1.5.1",
|
||||
}
|
||||
|
||||
def generate_signature(token_for_sign: str, path, body_or_query):
|
||||
"""
|
||||
生成请求签名
|
||||
|
||||
:param token_for_sign: 用于加密的token
|
||||
:param path: 请求路径(如 /api/v1/game/player/binding)
|
||||
:param body_or_query: GET用query字符串, POST用body字符串
|
||||
:return: (sign, 新的header_for_sign字典)
|
||||
"""
|
||||
|
||||
t = str(int(time.time()) - 2) # 时间戳, -2秒以防服务器时间不一致
|
||||
token_bytes = token_for_sign.encode("utf-8")
|
||||
header_ca = dict(header_for_sign)
|
||||
header_ca["timestamp"] = t
|
||||
header_ca_str = json.dumps(header_ca, separators=(",", ":"))
|
||||
s = path + body_or_query + t + header_ca_str # 拼接原始字符串
|
||||
# HMAC-SHA256 + MD5得到最终sign
|
||||
hex_s = hmac.new(token_bytes, s.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
md5 = hashlib.md5(hex_s.encode("utf-8")).hexdigest()
|
||||
return md5, header_ca
|
||||
|
||||
def get_sign_header(url: str, method, body, old_header, sign_token):
|
||||
"""
|
||||
获取带签名的请求头
|
||||
|
||||
:param url: 请求完整url
|
||||
:param method: 请求方式 GET/POST
|
||||
:param body: POST请求体或GET时为None
|
||||
:param old_header: 原始请求头
|
||||
:param sign_token: 当前会话的签名token
|
||||
:return: 新请求头
|
||||
"""
|
||||
|
||||
h = json.loads(json.dumps(old_header))
|
||||
p = parse.urlparse(url)
|
||||
if method.lower() == "get":
|
||||
sign, header_ca = generate_signature(sign_token, p.path, p.query)
|
||||
else:
|
||||
sign, header_ca = generate_signature(
|
||||
sign_token, p.path, json.dumps(body) if body else ""
|
||||
)
|
||||
h["sign"] = sign
|
||||
for i in header_ca:
|
||||
h[i] = header_ca[i]
|
||||
return h
|
||||
|
||||
def copy_header(cred):
|
||||
"""
|
||||
复制请求头并添加cred
|
||||
|
||||
:param cred: 当前会话的cred
|
||||
:return: 新的请求头
|
||||
"""
|
||||
v = json.loads(json.dumps(header))
|
||||
v["cred"] = cred
|
||||
return v
|
||||
|
||||
async def login_by_token(token_code):
|
||||
"""
|
||||
使用token一步步拿到cred和sign_token
|
||||
|
||||
:param token_code: 你的skyland token
|
||||
:return: (cred, sign_token)
|
||||
"""
|
||||
try:
|
||||
# token为json对象时提取data.content
|
||||
t = json.loads(token_code)
|
||||
token_code = t["data"]["content"]
|
||||
except:
|
||||
pass
|
||||
grant_code = await get_grant_code(token_code)
|
||||
return await get_cred(grant_code)
|
||||
|
||||
async def get_cred(grant):
|
||||
"""
|
||||
通过grant code获取cred和sign_token
|
||||
|
||||
:param grant: grant code
|
||||
:return: (cred, sign_token)
|
||||
"""
|
||||
|
||||
rsp = requests.post(
|
||||
cred_code_url,
|
||||
json={"code": grant, "kind": 1},
|
||||
headers=header_login,
|
||||
proxies=Config.get_proxies(),
|
||||
).json()
|
||||
if rsp["code"] != 0:
|
||||
raise Exception(f"获得cred失败: {rsp.get('message')}")
|
||||
sign_token = rsp["data"]["token"]
|
||||
cred = rsp["data"]["cred"]
|
||||
return cred, sign_token
|
||||
|
||||
async def get_grant_code(token):
|
||||
"""
|
||||
通过token获取grant code
|
||||
|
||||
:param token: 你的skyland token
|
||||
:return: grant code
|
||||
"""
|
||||
rsp = requests.post(
|
||||
grant_code_url,
|
||||
json={"appCode": app_code, "token": token, "type": 0},
|
||||
headers=header_login,
|
||||
proxies=Config.get_proxies(),
|
||||
).json()
|
||||
if rsp["status"] != 0:
|
||||
raise Exception(
|
||||
f"使用token: {token[:3]}******{token[-3:]} 获得认证代码失败: {rsp.get('msg')}"
|
||||
)
|
||||
return rsp["data"]["code"]
|
||||
|
||||
async def get_binding_list(cred, sign_token):
|
||||
"""
|
||||
查询已绑定的角色列表
|
||||
|
||||
:param cred: 当前cred
|
||||
:param sign_token: 当前sign_token
|
||||
:return: 角色列表
|
||||
"""
|
||||
v = []
|
||||
rsp = requests.get(
|
||||
binding_url,
|
||||
headers=get_sign_header(
|
||||
binding_url, "get", None, copy_header(cred), sign_token
|
||||
),
|
||||
proxies=Config.get_proxies(),
|
||||
).json()
|
||||
if rsp["code"] != 0:
|
||||
logger.error(f"请求角色列表出现问题: {rsp['message']}")
|
||||
if rsp.get("message") == "用户未登录":
|
||||
logger.error(f"用户登录可能失效了, 请重新登录!")
|
||||
return v
|
||||
# 只取明日方舟(arknights)的绑定账号
|
||||
for i in rsp["data"]["list"]:
|
||||
if i.get("appCode") != "arknights":
|
||||
continue
|
||||
v.extend(i.get("bindingList"))
|
||||
return v
|
||||
|
||||
async def do_sign(cred, sign_token) -> dict:
|
||||
"""
|
||||
对所有绑定的角色进行签到
|
||||
|
||||
:param cred: 当前cred
|
||||
:param sign_token: 当前sign_token
|
||||
:return: 签到结果字典
|
||||
"""
|
||||
|
||||
characters = await get_binding_list(cred, sign_token)
|
||||
result = {"成功": [], "重复": [], "失败": [], "总计": len(characters)}
|
||||
|
||||
for character in characters:
|
||||
|
||||
body = {
|
||||
"uid": character.get("uid"),
|
||||
"gameId": character.get("channelMasterId"),
|
||||
}
|
||||
rsp = requests.post(
|
||||
sign_url,
|
||||
headers=get_sign_header(
|
||||
sign_url, "post", body, copy_header(cred), sign_token
|
||||
),
|
||||
json=body,
|
||||
proxies=Config.get_proxies(),
|
||||
).json()
|
||||
|
||||
if rsp["code"] != 0:
|
||||
|
||||
result[
|
||||
"重复" if rsp.get("message") == "请勿重复签到!" else "失败"
|
||||
].append(
|
||||
f"{character.get("nickName")}({character.get("channelName")})"
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
result["成功"].append(
|
||||
f"{character.get("nickName")}({character.get("channelName")})"
|
||||
)
|
||||
|
||||
await asyncio.sleep(3)
|
||||
|
||||
return result
|
||||
|
||||
# 主流程
|
||||
try:
|
||||
# 拿到cred和sign_token
|
||||
cred, sign_token = await login_by_token(token)
|
||||
await asyncio.sleep(1)
|
||||
# 依次签到
|
||||
return await do_sign(cred, sign_token)
|
||||
except Exception as e:
|
||||
logger.exception(f"森空岛签到失败: {e}")
|
||||
return {"成功": [], "重复": [], "失败": [], "总计": 0}
|
||||
321
app/ui/Widget.py
321
app/ui/Widget.py
@@ -1,321 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA组件
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from PySide6.QtCore import Qt, QTime
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import QWidget, QHBoxLayout
|
||||
from qfluentwidgets import (
|
||||
LineEdit,
|
||||
PasswordLineEdit,
|
||||
MessageBoxBase,
|
||||
SubtitleLabel,
|
||||
SettingCard,
|
||||
SpinBox,
|
||||
FluentIconBase,
|
||||
Signal,
|
||||
ComboBox,
|
||||
CheckBox,
|
||||
qconfig,
|
||||
ConfigItem,
|
||||
TimeEdit,
|
||||
OptionsConfigItem,
|
||||
)
|
||||
from typing import Union, List
|
||||
|
||||
from app.services import Crypto
|
||||
|
||||
|
||||
class LineEditMessageBox(MessageBoxBase):
|
||||
"""输入对话框"""
|
||||
|
||||
def __init__(self, parent, title: str, content: str, mode: str):
|
||||
super().__init__(parent)
|
||||
self.title = SubtitleLabel(title)
|
||||
|
||||
if mode == "明文":
|
||||
self.input = LineEdit()
|
||||
self.input.setClearButtonEnabled(True)
|
||||
elif mode == "密码":
|
||||
self.input = PasswordLineEdit()
|
||||
|
||||
self.input.setPlaceholderText(content)
|
||||
|
||||
# 将组件添加到布局中
|
||||
self.viewLayout.addWidget(self.title)
|
||||
self.viewLayout.addWidget(self.input)
|
||||
|
||||
|
||||
class ComboBoxMessageBox(MessageBoxBase):
|
||||
"""选择对话框"""
|
||||
|
||||
def __init__(self, parent, title: str, content: List[str], list: List[List[str]]):
|
||||
super().__init__(parent)
|
||||
self.title = SubtitleLabel(title)
|
||||
|
||||
Widget = QWidget()
|
||||
Layout = QHBoxLayout(Widget)
|
||||
|
||||
self.input: List[ComboBox] = []
|
||||
|
||||
for i in range(len(content)):
|
||||
|
||||
self.input.append(ComboBox())
|
||||
self.input[i].addItems(list[i])
|
||||
self.input[i].setCurrentIndex(-1)
|
||||
self.input[i].setPlaceholderText(content[i])
|
||||
Layout.addWidget(self.input[i])
|
||||
|
||||
# 将组件添加到布局中
|
||||
self.viewLayout.addWidget(self.title)
|
||||
self.viewLayout.addWidget(Widget)
|
||||
|
||||
|
||||
class LineEditSettingCard(SettingCard):
|
||||
"""Setting card with LineEdit"""
|
||||
|
||||
textChanged = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text,
|
||||
icon: Union[str, QIcon, FluentIconBase],
|
||||
title,
|
||||
content=None,
|
||||
configItem: ConfigItem = None,
|
||||
parent=None,
|
||||
):
|
||||
|
||||
super().__init__(icon, title, content, parent)
|
||||
self.configItem = configItem
|
||||
self.LineEdit = LineEdit(self)
|
||||
self.LineEdit.setMinimumWidth(250)
|
||||
self.LineEdit.setPlaceholderText(text)
|
||||
|
||||
if configItem:
|
||||
self.setValue(qconfig.get(configItem))
|
||||
configItem.valueChanged.connect(self.setValue)
|
||||
|
||||
self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight)
|
||||
self.hBoxLayout.addSpacing(16)
|
||||
|
||||
self.LineEdit.textChanged.connect(self.__textChanged)
|
||||
|
||||
def __textChanged(self, content: str):
|
||||
self.setValue(content)
|
||||
self.textChanged.emit(content)
|
||||
|
||||
def setValue(self, content: str):
|
||||
if self.configItem:
|
||||
qconfig.set(self.configItem, content)
|
||||
|
||||
self.LineEdit.setText(content)
|
||||
|
||||
|
||||
class PasswordLineEditSettingCard(SettingCard):
|
||||
"""Setting card with PasswordLineEdit"""
|
||||
|
||||
textChanged = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text,
|
||||
icon: Union[str, QIcon, FluentIconBase],
|
||||
title,
|
||||
content=None,
|
||||
configItem: ConfigItem = None,
|
||||
parent=None,
|
||||
):
|
||||
|
||||
super().__init__(icon, title, content, parent)
|
||||
self.configItem = configItem
|
||||
self.LineEdit = PasswordLineEdit(self)
|
||||
self.LineEdit.setMinimumWidth(250)
|
||||
self.LineEdit.setPlaceholderText(text)
|
||||
|
||||
if configItem:
|
||||
self.setValue(qconfig.get(configItem))
|
||||
configItem.valueChanged.connect(self.setValue)
|
||||
|
||||
self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight)
|
||||
self.hBoxLayout.addSpacing(16)
|
||||
|
||||
self.LineEdit.textChanged.connect(self.__textChanged)
|
||||
|
||||
def __textChanged(self, content: str):
|
||||
self.setValue(Crypto.win_encryptor(content))
|
||||
self.textChanged.emit(content)
|
||||
|
||||
def setValue(self, content: str):
|
||||
if self.configItem:
|
||||
qconfig.set(self.configItem, content)
|
||||
|
||||
self.LineEdit.setText(Crypto.win_decryptor(content))
|
||||
|
||||
|
||||
class SpinBoxSettingCard(SettingCard):
|
||||
"""Setting card with SpinBox"""
|
||||
|
||||
textChanged = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
range: tuple[int, int],
|
||||
icon: Union[str, QIcon, FluentIconBase],
|
||||
title,
|
||||
content=None,
|
||||
configItem: ConfigItem = None,
|
||||
parent=None,
|
||||
):
|
||||
|
||||
super().__init__(icon, title, content, parent)
|
||||
self.configItem = configItem
|
||||
self.SpinBox = SpinBox(self)
|
||||
self.SpinBox.setRange(range[0], range[1])
|
||||
self.SpinBox.setMinimumWidth(150)
|
||||
|
||||
if configItem:
|
||||
self.setValue(qconfig.get(configItem))
|
||||
configItem.valueChanged.connect(self.setValue)
|
||||
|
||||
self.hBoxLayout.addWidget(self.SpinBox, 0, Qt.AlignRight)
|
||||
self.hBoxLayout.addSpacing(16)
|
||||
|
||||
self.SpinBox.valueChanged.connect(self.__valueChanged)
|
||||
|
||||
def __valueChanged(self, value: int):
|
||||
self.setValue(value)
|
||||
self.textChanged.emit(value)
|
||||
|
||||
def setValue(self, value: int):
|
||||
if self.configItem:
|
||||
qconfig.set(self.configItem, value)
|
||||
|
||||
self.SpinBox.setValue(value)
|
||||
|
||||
|
||||
class NoOptionComboBoxSettingCard(SettingCard):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
configItem: OptionsConfigItem,
|
||||
icon: Union[str, QIcon, FluentIconBase],
|
||||
title,
|
||||
content=None,
|
||||
value=None,
|
||||
texts=None,
|
||||
parent=None,
|
||||
):
|
||||
|
||||
super().__init__(icon, title, content, parent)
|
||||
self.configItem = configItem
|
||||
self.comboBox = ComboBox(self)
|
||||
self.comboBox.setMinimumWidth(250)
|
||||
self.hBoxLayout.addWidget(self.comboBox, 0, Qt.AlignRight)
|
||||
self.hBoxLayout.addSpacing(16)
|
||||
|
||||
self.optionToText = {o: t for o, t in zip(value, texts)}
|
||||
for text, option in zip(texts, value):
|
||||
self.comboBox.addItem(text, userData=option)
|
||||
|
||||
self.comboBox.setCurrentText(self.optionToText[qconfig.get(configItem)])
|
||||
self.comboBox.currentIndexChanged.connect(self._onCurrentIndexChanged)
|
||||
configItem.valueChanged.connect(self.setValue)
|
||||
|
||||
def _onCurrentIndexChanged(self, index: int):
|
||||
|
||||
qconfig.set(self.configItem, self.comboBox.itemData(index))
|
||||
|
||||
def setValue(self, value):
|
||||
if value not in self.optionToText:
|
||||
return
|
||||
|
||||
self.comboBox.setCurrentText(self.optionToText[value])
|
||||
qconfig.set(self.configItem, value)
|
||||
|
||||
|
||||
class TimeEditSettingCard(SettingCard):
|
||||
|
||||
enabledChanged = Signal(bool)
|
||||
timeChanged = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
icon: Union[str, QIcon, FluentIconBase],
|
||||
title,
|
||||
content=None,
|
||||
configItem_bool: ConfigItem = None,
|
||||
configItem_time: ConfigItem = None,
|
||||
parent=None,
|
||||
):
|
||||
|
||||
super().__init__(icon, title, content, parent)
|
||||
self.configItem_bool = configItem_bool
|
||||
self.configItem_time = configItem_time
|
||||
self.CheckBox = CheckBox(self)
|
||||
self.CheckBox.setTristate(False)
|
||||
self.TimeEdit = TimeEdit(self)
|
||||
self.TimeEdit.setDisplayFormat("HH:mm")
|
||||
self.TimeEdit.setMinimumWidth(150)
|
||||
|
||||
if configItem_bool:
|
||||
self.setValue_bool(qconfig.get(configItem_bool))
|
||||
configItem_bool.valueChanged.connect(self.setValue_bool)
|
||||
|
||||
if configItem_time:
|
||||
self.setValue_time(qconfig.get(configItem_time))
|
||||
configItem_time.valueChanged.connect(self.setValue_time)
|
||||
|
||||
self.hBoxLayout.addWidget(self.CheckBox, 0, Qt.AlignRight)
|
||||
self.hBoxLayout.addWidget(self.TimeEdit, 0, Qt.AlignRight)
|
||||
self.hBoxLayout.addSpacing(16)
|
||||
|
||||
self.CheckBox.stateChanged.connect(self.__enableChanged)
|
||||
self.TimeEdit.timeChanged.connect(self.__timeChanged)
|
||||
|
||||
def __timeChanged(self, value: QTime):
|
||||
self.setValue_time(value.toString("HH:mm"))
|
||||
self.timeChanged.emit(value.toString("HH:mm"))
|
||||
|
||||
def __enableChanged(self, value: int):
|
||||
if value == 0:
|
||||
self.setValue_bool(False)
|
||||
self.enabledChanged.emit(False)
|
||||
else:
|
||||
self.setValue_bool(True)
|
||||
self.enabledChanged.emit(True)
|
||||
|
||||
def setValue_bool(self, value: bool):
|
||||
if self.configItem_bool:
|
||||
qconfig.set(self.configItem_bool, value)
|
||||
|
||||
self.CheckBox.setChecked(value)
|
||||
|
||||
def setValue_time(self, value: str):
|
||||
if self.configItem_time:
|
||||
qconfig.set(self.configItem_time, value)
|
||||
|
||||
self.TimeEdit.setTime(QTime.fromString(value, "HH:mm"))
|
||||
@@ -1,34 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA图形化界面包
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
__version__ = "4.2.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .main_window import AUTO_MAA
|
||||
|
||||
__all__ = ["AUTO_MAA"]
|
||||
@@ -1,456 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA调度中枢界面
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QStackedWidget,
|
||||
QHBoxLayout,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
CardWidget,
|
||||
IconWidget,
|
||||
BodyLabel,
|
||||
Pivot,
|
||||
ScrollArea,
|
||||
FluentIcon,
|
||||
HeaderCardWidget,
|
||||
FluentIcon,
|
||||
TextBrowser,
|
||||
ComboBox,
|
||||
SubtitleLabel,
|
||||
PushButton,
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QTextCursor
|
||||
from typing import List, Dict
|
||||
import json
|
||||
|
||||
|
||||
from app.core import Config, TaskManager, Task, MainInfoBar
|
||||
|
||||
|
||||
class DispatchCenter(QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("调度中枢")
|
||||
|
||||
self.pivot = Pivot(self)
|
||||
self.stackedWidget = QStackedWidget(self)
|
||||
self.Layout = QVBoxLayout(self)
|
||||
|
||||
self.script_list: Dict[str, DispatchBox] = {}
|
||||
|
||||
dispatch_box = DispatchBox("主调度台", self)
|
||||
self.script_list["主调度台"] = dispatch_box
|
||||
self.stackedWidget.addWidget(self.script_list["主调度台"])
|
||||
self.pivot.addItem(
|
||||
routeKey="主调度台",
|
||||
text="主调度台",
|
||||
onClick=self.update_top_bar,
|
||||
icon=FluentIcon.CAFE,
|
||||
)
|
||||
|
||||
self.Layout.addWidget(self.pivot, 0, Qt.AlignHCenter)
|
||||
self.Layout.addWidget(self.stackedWidget)
|
||||
self.Layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.pivot.currentItemChanged.connect(
|
||||
lambda index: self.stackedWidget.setCurrentWidget(self.script_list[index])
|
||||
)
|
||||
|
||||
def add_board(self, task: Task) -> None:
|
||||
"""添加一个调度台界面"""
|
||||
|
||||
dispatch_box = DispatchBox(task.name, self)
|
||||
|
||||
dispatch_box.top_bar.button.clicked.connect(
|
||||
lambda: TaskManager.stop_task(task.name)
|
||||
)
|
||||
|
||||
task.create_task_list.connect(dispatch_box.info.task.create_task)
|
||||
task.create_user_list.connect(dispatch_box.info.user.create_user)
|
||||
task.update_task_list.connect(dispatch_box.info.task.update_task)
|
||||
task.update_user_list.connect(dispatch_box.info.user.update_user)
|
||||
task.update_log_text.connect(dispatch_box.info.log_text.text.setText)
|
||||
task.accomplish.connect(lambda: self.del_board(f"调度台_{task.name}"))
|
||||
|
||||
self.script_list[f"调度台_{task.name}"] = dispatch_box
|
||||
|
||||
self.stackedWidget.addWidget(self.script_list[f"调度台_{task.name}"])
|
||||
|
||||
self.pivot.addItem(routeKey=f"调度台_{task.name}", text=f"调度台 {task.name}")
|
||||
|
||||
def del_board(self, name: str) -> None:
|
||||
"""删除指定子界面"""
|
||||
|
||||
self.pivot.setCurrentItem("主调度台")
|
||||
self.stackedWidget.removeWidget(self.script_list[name])
|
||||
self.script_list[name].deleteLater()
|
||||
self.pivot.removeWidget(name)
|
||||
|
||||
def connect_main_board(self, task: Task) -> None:
|
||||
"""连接主调度台"""
|
||||
|
||||
self.script_list["主调度台"].top_bar.Lable.setText(
|
||||
f"{task.name} - {task.mode.replace("_主调度台","")}模式"
|
||||
)
|
||||
self.script_list["主调度台"].top_bar.Lable.show()
|
||||
self.script_list["主调度台"].top_bar.object.hide()
|
||||
self.script_list["主调度台"].top_bar.mode.hide()
|
||||
self.script_list["主调度台"].top_bar.button.clicked.disconnect()
|
||||
self.script_list["主调度台"].top_bar.button.setText("中止任务")
|
||||
self.script_list["主调度台"].top_bar.button.clicked.connect(
|
||||
lambda: TaskManager.stop_task(task.name)
|
||||
)
|
||||
task.create_task_list.connect(
|
||||
self.script_list["主调度台"].info.task.create_task
|
||||
)
|
||||
task.create_user_list.connect(
|
||||
self.script_list["主调度台"].info.user.create_user
|
||||
)
|
||||
task.update_task_list.connect(
|
||||
self.script_list["主调度台"].info.task.update_task
|
||||
)
|
||||
task.update_user_list.connect(
|
||||
self.script_list["主调度台"].info.user.update_user
|
||||
)
|
||||
task.update_log_text.connect(
|
||||
self.script_list["主调度台"].info.log_text.text.setText
|
||||
)
|
||||
task.accomplish.connect(lambda: self.disconnect_main_board(task.name))
|
||||
|
||||
def disconnect_main_board(self, name: str) -> None:
|
||||
"""断开主调度台"""
|
||||
|
||||
self.script_list["主调度台"].top_bar.Lable.hide()
|
||||
self.script_list["主调度台"].top_bar.object.show()
|
||||
self.script_list["主调度台"].top_bar.mode.show()
|
||||
self.script_list["主调度台"].top_bar.button.clicked.disconnect()
|
||||
self.script_list["主调度台"].top_bar.button.setText("开始任务")
|
||||
self.script_list["主调度台"].top_bar.button.clicked.connect(
|
||||
self.script_list["主调度台"].top_bar.start_task
|
||||
)
|
||||
self.script_list["主调度台"].info.log_text.text.setText(
|
||||
Config.get_history(name)["History"]
|
||||
)
|
||||
|
||||
def update_top_bar(self):
|
||||
"""更新顶栏"""
|
||||
|
||||
list = []
|
||||
queue_numb, member_numb = 0, 0
|
||||
|
||||
if (Config.app_path / "config/QueueConfig").exists():
|
||||
for json_file in (Config.app_path / "config/QueueConfig").glob("*.json"):
|
||||
list.append(f"队列 - {json_file.stem}")
|
||||
queue_numb += 1
|
||||
|
||||
if (Config.app_path / "config/MaaConfig").exists():
|
||||
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
|
||||
if subdir.is_dir():
|
||||
list.append(f"实例 - Maa - {subdir.name}")
|
||||
member_numb += 1
|
||||
|
||||
self.script_list["主调度台"].top_bar.object.clear()
|
||||
self.script_list["主调度台"].top_bar.object.addItems(list)
|
||||
self.script_list["主调度台"].top_bar.mode.clear()
|
||||
self.script_list["主调度台"].top_bar.mode.addItems(["自动代理", "人工排查"])
|
||||
|
||||
if queue_numb == 1:
|
||||
self.script_list["主调度台"].top_bar.object.setCurrentIndex(0)
|
||||
elif member_numb == 1:
|
||||
self.script_list["主调度台"].top_bar.object.setCurrentIndex(queue_numb)
|
||||
else:
|
||||
self.script_list["主调度台"].top_bar.object.setCurrentIndex(-1)
|
||||
self.script_list["主调度台"].top_bar.mode.setCurrentIndex(0)
|
||||
|
||||
|
||||
class DispatchBox(QWidget):
|
||||
|
||||
def __init__(self, name: str, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName(name)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
scrollArea = ScrollArea()
|
||||
scrollArea.setWidgetResizable(True)
|
||||
|
||||
content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(content_widget)
|
||||
|
||||
self.top_bar = self.DispatchTopBar(self, name)
|
||||
self.info = self.DispatchInfoCard(self)
|
||||
|
||||
content_layout.addWidget(self.top_bar)
|
||||
content_layout.addWidget(self.info)
|
||||
|
||||
scrollArea.setWidget(content_widget)
|
||||
|
||||
layout.addWidget(scrollArea)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
class DispatchTopBar(CardWidget):
|
||||
|
||||
def __init__(self, parent=None, name: str = None):
|
||||
super().__init__(parent)
|
||||
|
||||
Layout = QHBoxLayout(self)
|
||||
|
||||
if name == "主调度台":
|
||||
|
||||
self.Lable = SubtitleLabel("", self)
|
||||
self.Lable.hide()
|
||||
self.object = ComboBox()
|
||||
self.object.setPlaceholderText("请选择调度对象")
|
||||
self.mode = ComboBox()
|
||||
self.mode.setPlaceholderText("请选择调度模式")
|
||||
|
||||
self.button = PushButton("开始任务")
|
||||
self.button.clicked.connect(self.start_task)
|
||||
|
||||
Layout.addWidget(self.Lable)
|
||||
Layout.addWidget(self.object)
|
||||
Layout.addWidget(self.mode)
|
||||
Layout.addStretch(1)
|
||||
Layout.addWidget(self.button)
|
||||
|
||||
else:
|
||||
|
||||
self.Lable = SubtitleLabel(name, self)
|
||||
self.button = PushButton("中止任务")
|
||||
|
||||
Layout.addWidget(self.Lable)
|
||||
Layout.addStretch(1)
|
||||
Layout.addWidget(self.button)
|
||||
|
||||
def start_task(self):
|
||||
"""开始任务"""
|
||||
|
||||
if self.object.currentIndex() == -1:
|
||||
logger.warning("未选择调度对象")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度对象", "请选择后再开始任务", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if self.mode.currentIndex() == -1:
|
||||
logger.warning("未选择调度模式")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度模式", "请选择后再开始任务", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
name = self.object.currentText().split(" - ")[-1]
|
||||
|
||||
if name in Config.running_list:
|
||||
logger.warning(f"任务已存在:{name}")
|
||||
MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
|
||||
return None
|
||||
|
||||
if self.object.currentText().split(" - ")[0] == "队列":
|
||||
|
||||
with (Config.app_path / f"config/QueueConfig/{name}.json").open(
|
||||
mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
info = json.load(f)
|
||||
|
||||
logger.info(f"用户添加任务:{name}")
|
||||
TaskManager.add_task(f"{self.mode.currentText()}_主调度台", name, info)
|
||||
|
||||
elif self.object.currentText().split(" - ")[0] == "实例":
|
||||
|
||||
if self.object.currentText().split(" - ")[1] == "Maa":
|
||||
|
||||
info = {"Queue": {"Member_1": name}}
|
||||
|
||||
logger.info(f"用户添加任务:{name}")
|
||||
TaskManager.add_task(
|
||||
f"{self.mode.currentText()}_主调度台", "自定义队列", info
|
||||
)
|
||||
|
||||
class DispatchInfoCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("调度信息")
|
||||
|
||||
self.task = self.TaskInfoCard(self)
|
||||
self.user = self.UserInfoCard(self)
|
||||
self.log_text = self.LogCard(self)
|
||||
|
||||
self.viewLayout.addWidget(self.task)
|
||||
self.viewLayout.addWidget(self.user)
|
||||
self.viewLayout.addWidget(self.log_text)
|
||||
|
||||
self.viewLayout.setStretch(0, 1)
|
||||
self.viewLayout.setStretch(1, 1)
|
||||
self.viewLayout.setStretch(2, 5)
|
||||
|
||||
def update_board(self, task_list: list, user_list: list, log: str):
|
||||
"""更新调度信息"""
|
||||
|
||||
self.task.update_task(task_list)
|
||||
self.user.update_user(user_list)
|
||||
self.log_text.text.setText(log)
|
||||
|
||||
class TaskInfoCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("任务队列")
|
||||
|
||||
self.Layout = QVBoxLayout()
|
||||
self.viewLayout.addLayout(self.Layout)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
|
||||
self.task_cards: List[ItemCard] = []
|
||||
|
||||
def create_task(self, task_list: list):
|
||||
"""创建任务队列"""
|
||||
|
||||
while self.Layout.count() > 0:
|
||||
item = self.Layout.takeAt(0)
|
||||
if item.spacerItem():
|
||||
self.Layout.removeItem(item.spacerItem())
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
self.task_cards = []
|
||||
|
||||
for task in task_list:
|
||||
|
||||
self.task_cards.append(ItemCard(task))
|
||||
self.Layout.addWidget(self.task_cards[-1])
|
||||
|
||||
self.Layout.addStretch(1)
|
||||
|
||||
def update_task(self, task_list: list):
|
||||
"""更新任务队列"""
|
||||
|
||||
for i in range(len(task_list)):
|
||||
|
||||
self.task_cards[i].update_status(task_list[i][1])
|
||||
|
||||
class UserInfoCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("用户队列")
|
||||
|
||||
self.Layout = QVBoxLayout()
|
||||
self.viewLayout.addLayout(self.Layout)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
|
||||
self.user_cards: List[ItemCard] = []
|
||||
|
||||
def create_user(self, user_list: list):
|
||||
"""创建用户队列"""
|
||||
|
||||
while self.Layout.count() > 0:
|
||||
item = self.Layout.takeAt(0)
|
||||
if item.spacerItem():
|
||||
self.Layout.removeItem(item.spacerItem())
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
self.user_cards = []
|
||||
|
||||
for user in user_list:
|
||||
|
||||
self.user_cards.append(ItemCard(user))
|
||||
self.Layout.addWidget(self.user_cards[-1])
|
||||
|
||||
self.Layout.addStretch(1)
|
||||
|
||||
def update_user(self, user_list: list):
|
||||
"""更新用户队列"""
|
||||
|
||||
for i in range(len(user_list)):
|
||||
|
||||
self.user_cards[i].Label.setText(user_list[i][0])
|
||||
self.user_cards[i].update_status(user_list[i][1])
|
||||
|
||||
class LogCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("日志")
|
||||
|
||||
self.text = TextBrowser()
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
self.viewLayout.addWidget(self.text)
|
||||
|
||||
self.text.textChanged.connect(self.to_end)
|
||||
|
||||
def to_end(self):
|
||||
"""滚动到底部"""
|
||||
|
||||
self.text.moveCursor(QTextCursor.End)
|
||||
self.text.ensureCursorVisible()
|
||||
|
||||
|
||||
class ItemCard(CardWidget):
|
||||
|
||||
def __init__(self, task_item: list, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.Layout = QHBoxLayout(self)
|
||||
|
||||
self.Label = BodyLabel(task_item[0], self)
|
||||
self.icon = IconWidget(FluentIcon.MORE, self)
|
||||
self.icon.setFixedSize(16, 16)
|
||||
self.update_status(task_item[1])
|
||||
|
||||
self.Layout.addWidget(self.icon)
|
||||
self.Layout.addWidget(self.Label)
|
||||
self.Layout.addStretch(1)
|
||||
|
||||
def update_status(self, status: str):
|
||||
|
||||
if status == "完成":
|
||||
self.icon.setIcon(FluentIcon.ACCEPT)
|
||||
self.Label.setTextColor("#0eb840", "#0eb840")
|
||||
elif status == "等待":
|
||||
self.icon.setIcon(FluentIcon.MORE)
|
||||
self.Label.setTextColor("#7397ab", "#7397ab")
|
||||
elif status == "运行":
|
||||
self.icon.setIcon(FluentIcon.PLAY)
|
||||
self.Label.setTextColor("#2e4e7e", "#2e4e7e")
|
||||
elif status == "跳过":
|
||||
self.icon.setIcon(FluentIcon.REMOVE)
|
||||
self.Label.setTextColor("#606060", "#d2d2d2")
|
||||
elif status == "异常":
|
||||
self.icon.setIcon(FluentIcon.CLOSE)
|
||||
self.Label.setTextColor("#ff2121", "#ff2121")
|
||||
@@ -1,356 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA主界面
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import QSystemTrayIcon
|
||||
from qfluentwidgets import (
|
||||
Action,
|
||||
PushButton,
|
||||
SystemTrayMenu,
|
||||
SplashScreen,
|
||||
FluentIcon,
|
||||
InfoBar,
|
||||
InfoBarPosition,
|
||||
setTheme,
|
||||
isDarkTheme,
|
||||
SystemThemeListener,
|
||||
Theme,
|
||||
MSFluentWindow,
|
||||
NavigationItemPosition,
|
||||
qconfig,
|
||||
)
|
||||
from PySide6.QtGui import QIcon, QCloseEvent
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
import json
|
||||
|
||||
from app.core import Config, TaskManager, MainTimer, MainInfoBar
|
||||
from app.services import Notify, Crypto, System
|
||||
from .setting import Setting
|
||||
from .member_manager import MemberManager
|
||||
from .queue_manager import QueueManager
|
||||
from .dispatch_center import DispatchCenter
|
||||
|
||||
|
||||
class AUTO_MAA(MSFluentWindow):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowIcon(QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")))
|
||||
self.setWindowTitle("AUTO_MAA")
|
||||
|
||||
setTheme(Theme.AUTO, lazy=True)
|
||||
|
||||
self.splashScreen = SplashScreen(self.windowIcon(), self)
|
||||
self.show_ui("显示主窗口", if_quick=True)
|
||||
|
||||
MainInfoBar.main_window = self.window()
|
||||
System.main_window = self.window()
|
||||
|
||||
# 创建主窗口
|
||||
self.setting = Setting(self)
|
||||
self.member_manager = MemberManager(self)
|
||||
self.queue_manager = QueueManager(self)
|
||||
self.dispatch_center = DispatchCenter(self)
|
||||
|
||||
self.addSubInterface(
|
||||
self.setting,
|
||||
FluentIcon.SETTING,
|
||||
"设置",
|
||||
FluentIcon.SETTING,
|
||||
NavigationItemPosition.BOTTOM,
|
||||
)
|
||||
self.addSubInterface(
|
||||
self.member_manager,
|
||||
FluentIcon.ROBOT,
|
||||
"脚本管理",
|
||||
FluentIcon.ROBOT,
|
||||
NavigationItemPosition.TOP,
|
||||
)
|
||||
self.addSubInterface(
|
||||
self.queue_manager,
|
||||
FluentIcon.BOOK_SHELF,
|
||||
"调度队列",
|
||||
FluentIcon.BOOK_SHELF,
|
||||
NavigationItemPosition.TOP,
|
||||
)
|
||||
self.addSubInterface(
|
||||
self.dispatch_center,
|
||||
FluentIcon.IOT,
|
||||
"调度中心",
|
||||
FluentIcon.IOT,
|
||||
NavigationItemPosition.TOP,
|
||||
)
|
||||
self.stackedWidget.currentChanged.connect(
|
||||
lambda index: (self.member_manager.refresh() if index == 1 else None)
|
||||
)
|
||||
self.stackedWidget.currentChanged.connect(
|
||||
lambda index: self.queue_manager.refresh() if index == 2 else None
|
||||
)
|
||||
self.stackedWidget.currentChanged.connect(
|
||||
lambda index: (
|
||||
self.dispatch_center.pivot.setCurrentItem("主调度台")
|
||||
if index == 3
|
||||
else None
|
||||
)
|
||||
)
|
||||
self.stackedWidget.currentChanged.connect(
|
||||
lambda index: (
|
||||
self.dispatch_center.update_top_bar() if index == 3 else None
|
||||
)
|
||||
)
|
||||
|
||||
# 创建系统托盘及其菜单
|
||||
self.tray = QSystemTrayIcon(
|
||||
QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")),
|
||||
self,
|
||||
)
|
||||
self.tray.setToolTip("AUTO_MAA")
|
||||
self.tray_menu = SystemTrayMenu("AUTO_MAA", self)
|
||||
|
||||
# 显示主界面菜单项
|
||||
self.tray_menu.addAction(
|
||||
Action(
|
||||
FluentIcon.CAFE,
|
||||
"显示主界面",
|
||||
triggered=lambda: self.show_ui("显示主窗口"),
|
||||
)
|
||||
)
|
||||
self.tray_menu.addSeparator()
|
||||
|
||||
# 开始任务菜单项
|
||||
self.tray_menu.addActions(
|
||||
[
|
||||
Action(FluentIcon.PLAY, "运行自动代理", triggered=self.start_main_task),
|
||||
Action(
|
||||
FluentIcon.PAUSE,
|
||||
"中止所有任务",
|
||||
triggered=lambda: TaskManager.stop_task("ALL"),
|
||||
),
|
||||
]
|
||||
)
|
||||
self.tray_menu.addSeparator()
|
||||
|
||||
# 退出主程序菜单项
|
||||
self.tray_menu.addAction(
|
||||
Action(FluentIcon.POWER_BUTTON, "退出主程序", triggered=self.window().close)
|
||||
)
|
||||
|
||||
# 设置托盘菜单
|
||||
self.tray.setContextMenu(self.tray_menu)
|
||||
self.tray.activated.connect(self.on_tray_activated)
|
||||
|
||||
TaskManager.create_gui.connect(self.dispatch_center.add_board)
|
||||
TaskManager.connect_gui.connect(self.dispatch_center.connect_main_board)
|
||||
self.setting.ui.card_IfShowTray.checkedChanged.connect(
|
||||
lambda: self.show_ui("配置托盘")
|
||||
)
|
||||
self.setting.ui.card_IfToTray.checkedChanged.connect(self.set_min_method)
|
||||
|
||||
self.splashScreen.finish()
|
||||
|
||||
self.themeListener = SystemThemeListener(self)
|
||||
self.themeListener.systemThemeChanged.connect(self.switch_theme)
|
||||
self.themeListener.start()
|
||||
|
||||
def switch_theme(self):
|
||||
"""切换主题"""
|
||||
|
||||
setTheme(Theme.AUTO, lazy=True)
|
||||
QTimer.singleShot(100, lambda: setTheme(Theme.AUTO, lazy=True))
|
||||
|
||||
# 云母特效启用时需要增加重试机制
|
||||
if self.isMicaEffectEnabled():
|
||||
QTimer.singleShot(
|
||||
100,
|
||||
lambda: self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()),
|
||||
)
|
||||
|
||||
def start_up_task(self) -> None:
|
||||
"""启动时任务"""
|
||||
|
||||
# 加载配置
|
||||
qconfig.load(Config.config_path, Config.global_config)
|
||||
Config.global_config.save()
|
||||
|
||||
# 检查密码
|
||||
self.setting.check_PASSWORD()
|
||||
|
||||
# 获取公告
|
||||
self.setting.show_notice(if_show=False)
|
||||
|
||||
# 检查更新
|
||||
if Config.global_config.get(Config.global_config.update_IfAutoUpdate):
|
||||
result = self.setting.get_update_info()
|
||||
if result == "已是最新版本~":
|
||||
MainInfoBar.push_info_bar("success", "更新检查", result, 3000)
|
||||
else:
|
||||
info = InfoBar.info(
|
||||
title="更新检查",
|
||||
content=result,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.BOTTOM_LEFT,
|
||||
duration=-1,
|
||||
parent=self,
|
||||
)
|
||||
Up = PushButton("更新")
|
||||
Up.clicked.connect(lambda: self.setting.get_update(if_question=False))
|
||||
Up.clicked.connect(info.close)
|
||||
info.addWidget(Up)
|
||||
info.show()
|
||||
|
||||
# 直接运行主任务
|
||||
if Config.global_config.get(Config.global_config.start_IfRunDirectly):
|
||||
|
||||
self.start_main_task()
|
||||
|
||||
def set_min_method(self) -> None:
|
||||
"""设置最小化方法"""
|
||||
|
||||
if Config.global_config.get(Config.global_config.ui_IfToTray):
|
||||
|
||||
self.titleBar.minBtn.clicked.disconnect()
|
||||
self.titleBar.minBtn.clicked.connect(lambda: self.show_ui("隐藏到托盘"))
|
||||
|
||||
else:
|
||||
|
||||
self.titleBar.minBtn.clicked.disconnect()
|
||||
self.titleBar.minBtn.clicked.connect(self.window().showMinimized)
|
||||
|
||||
def on_tray_activated(self, reason):
|
||||
"""双击返回主界面"""
|
||||
if reason == QSystemTrayIcon.DoubleClick:
|
||||
self.show_ui("显示主窗口")
|
||||
|
||||
def start_main_task(self) -> None:
|
||||
"""启动主任务"""
|
||||
|
||||
if (Config.app_path / "config/QueueConfig/调度队列_1.json").exists():
|
||||
|
||||
with (Config.app_path / "config/QueueConfig/调度队列_1.json").open(
|
||||
mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
info = json.load(f)
|
||||
|
||||
logger.info("自动添加任务:调度队列_1")
|
||||
TaskManager.add_task("自动代理_主调度台", "主任务队列", info)
|
||||
|
||||
elif (Config.app_path / "config/MaaConfig/脚本_1").exists():
|
||||
|
||||
info = {"Queue": {"Member_1": "脚本_1"}}
|
||||
|
||||
logger.info("自动添加任务:脚本_1")
|
||||
TaskManager.add_task("自动代理_主调度台", "主任务队列", info)
|
||||
|
||||
else:
|
||||
|
||||
logger.worning("启动主任务失败:未找到有效的主任务配置文件")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "启动主任务失败", "“调度队列_1”与“脚本_1”均不存在", -1
|
||||
)
|
||||
|
||||
def show_ui(self, mode: str, if_quick: bool = False) -> None:
|
||||
"""配置窗口状态"""
|
||||
|
||||
if mode == "显示主窗口":
|
||||
|
||||
# 配置主窗口
|
||||
size = list(
|
||||
map(
|
||||
int,
|
||||
Config.global_config.get(Config.global_config.ui_size).split("x"),
|
||||
)
|
||||
)
|
||||
location = list(
|
||||
map(
|
||||
int,
|
||||
Config.global_config.get(Config.global_config.ui_location).split(
|
||||
"x"
|
||||
),
|
||||
)
|
||||
)
|
||||
self.window().setGeometry(location[0], location[1], size[0], size[1])
|
||||
self.window().show()
|
||||
if not if_quick:
|
||||
if Config.global_config.get(Config.global_config.ui_maximized):
|
||||
self.window().showMaximized()
|
||||
self.set_min_method()
|
||||
self.show_ui("配置托盘")
|
||||
|
||||
elif mode == "配置托盘":
|
||||
|
||||
if Config.global_config.get(Config.global_config.ui_IfShowTray):
|
||||
self.tray.show()
|
||||
else:
|
||||
self.tray.hide()
|
||||
|
||||
elif mode == "隐藏到托盘":
|
||||
|
||||
# 保存窗口相关属性
|
||||
if not self.window().isMaximized():
|
||||
|
||||
Config.global_config.set(
|
||||
Config.global_config.ui_size,
|
||||
f"{self.geometry().width()}x{self.geometry().height()}",
|
||||
)
|
||||
Config.global_config.set(
|
||||
Config.global_config.ui_location,
|
||||
f"{self.geometry().x()}x{self.geometry().y()}",
|
||||
)
|
||||
Config.global_config.set(
|
||||
Config.global_config.ui_maximized, self.window().isMaximized()
|
||||
)
|
||||
Config.global_config.save()
|
||||
|
||||
# 隐藏主窗口
|
||||
if not if_quick:
|
||||
|
||||
self.window().hide()
|
||||
self.tray.show()
|
||||
|
||||
def closeEvent(self, event: QCloseEvent):
|
||||
"""清理残余进程"""
|
||||
|
||||
self.show_ui("隐藏到托盘", if_quick=True)
|
||||
|
||||
# 清理各功能线程
|
||||
MainTimer.Timer.stop()
|
||||
MainTimer.Timer.deleteLater()
|
||||
TaskManager.stop_task("ALL")
|
||||
|
||||
# 关闭数据库连接
|
||||
Config.close_database()
|
||||
|
||||
# 关闭主题监听
|
||||
self.themeListener.terminate()
|
||||
self.themeListener.deleteLater()
|
||||
|
||||
logger.info("AUTO_MAA主程序关闭")
|
||||
logger.info("----------------END----------------")
|
||||
|
||||
event.accept()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,672 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA调度队列界面
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QStackedWidget,
|
||||
QHBoxLayout,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
Action,
|
||||
qconfig,
|
||||
Pivot,
|
||||
ScrollArea,
|
||||
FluentIcon,
|
||||
MessageBox,
|
||||
HeaderCardWidget,
|
||||
TextBrowser,
|
||||
CommandBar,
|
||||
SwitchSettingCard,
|
||||
ComboBoxSettingCard,
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from typing import List
|
||||
import json
|
||||
import shutil
|
||||
|
||||
from app.core import Config, MainInfoBar
|
||||
from .Widget import (
|
||||
LineEditSettingCard,
|
||||
TimeEditSettingCard,
|
||||
NoOptionComboBoxSettingCard,
|
||||
)
|
||||
|
||||
|
||||
class QueueManager(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("调度队列")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.tools = CommandBar()
|
||||
|
||||
self.queue_manager = QueueSettingBox(self)
|
||||
|
||||
# 逐个添加动作
|
||||
self.tools.addActions(
|
||||
[
|
||||
Action(
|
||||
FluentIcon.ADD_TO, "新建调度队列", triggered=self.add_setting_box
|
||||
),
|
||||
Action(
|
||||
FluentIcon.REMOVE_FROM,
|
||||
"删除调度队列",
|
||||
triggered=self.del_setting_box,
|
||||
),
|
||||
]
|
||||
)
|
||||
self.tools.addSeparator()
|
||||
self.tools.addActions(
|
||||
[
|
||||
Action(
|
||||
FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_setting_box
|
||||
),
|
||||
Action(
|
||||
FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_setting_box
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
layout.addWidget(self.tools)
|
||||
layout.addWidget(self.queue_manager)
|
||||
|
||||
def add_setting_box(self):
|
||||
"""添加一个调度队列"""
|
||||
|
||||
index = len(self.queue_manager.search_queue()) + 1
|
||||
|
||||
qconfig.load(
|
||||
Config.app_path / f"config/QueueConfig/调度队列_{index}.json",
|
||||
Config.queue_config,
|
||||
)
|
||||
Config.clear_queue_config()
|
||||
Config.queue_config.save()
|
||||
|
||||
self.queue_manager.add_QueueSettingBox(index)
|
||||
self.queue_manager.switch_SettingBox(index)
|
||||
|
||||
def del_setting_box(self):
|
||||
"""删除一个调度队列实例"""
|
||||
|
||||
name = self.queue_manager.pivot.currentRouteKey()
|
||||
|
||||
if name == None:
|
||||
logger.warning("未选择调度队列")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if name in Config.running_list:
|
||||
logger.warning("调度队列正在运行")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "调度队列正在运行", "请先停止调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
choice = MessageBox(
|
||||
"确认",
|
||||
f"确定要删除 {name} 吗?",
|
||||
self.window(),
|
||||
)
|
||||
if choice.exec():
|
||||
|
||||
queue_list = self.queue_manager.search_queue()
|
||||
move_list = [_ for _ in queue_list if int(_[0][5:]) > int(name[5:])]
|
||||
|
||||
index = max(int(name[5:]) - 1, 1)
|
||||
|
||||
self.queue_manager.clear_SettingBox()
|
||||
|
||||
(Config.app_path / f"config/QueueConfig/{name}.json").unlink()
|
||||
for queue in move_list:
|
||||
if (Config.app_path / f"config/QueueConfig/{queue[0]}.json").exists():
|
||||
(Config.app_path / f"config/QueueConfig/{queue[0]}.json").rename(
|
||||
Config.app_path
|
||||
/ f"config/QueueConfig/调度队列_{int(queue[0][5:])-1}.json",
|
||||
)
|
||||
|
||||
self.queue_manager.show_SettingBox(index)
|
||||
|
||||
def left_setting_box(self):
|
||||
"""向左移动调度队列实例"""
|
||||
|
||||
name = self.queue_manager.pivot.currentRouteKey()
|
||||
|
||||
if name == None:
|
||||
logger.warning("未选择调度队列")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
index = int(name[5:])
|
||||
|
||||
if index == 1:
|
||||
logger.warning("向左移动调度队列时已到达最左端")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "已经是第一个调度队列", "无法向左移动", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if name in Config.running_list or f"调度队列_{index-1}" in Config.running_list:
|
||||
logger.warning("相关调度队列正在运行")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
self.queue_manager.clear_SettingBox()
|
||||
|
||||
(Config.app_path / f"config/QueueConfig/调度队列_{index}.json").rename(
|
||||
Config.app_path / f"config/QueueConfig/调度队列_0.json",
|
||||
)
|
||||
shutil.move(
|
||||
str(Config.app_path / f"config/QueueConfig/调度队列_{index-1}.json"),
|
||||
str(Config.app_path / f"config/QueueConfig/调度队列_{index}.json"),
|
||||
)
|
||||
(Config.app_path / f"config/QueueConfig/调度队列_0.json").rename(
|
||||
Config.app_path / f"config/QueueConfig/调度队列_{index-1}.json",
|
||||
)
|
||||
|
||||
self.queue_manager.show_SettingBox(index - 1)
|
||||
|
||||
def right_setting_box(self):
|
||||
"""向右移动调度队列实例"""
|
||||
|
||||
name = self.queue_manager.pivot.currentRouteKey()
|
||||
|
||||
if name == None:
|
||||
logger.warning("未选择调度队列")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
queue_list = self.queue_manager.search_queue()
|
||||
index = int(name[5:])
|
||||
|
||||
if index == len(queue_list):
|
||||
logger.warning("向右移动调度队列时已到达最右端")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "已经是最后一个调度队列", "无法向右移动", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if name in Config.running_list or f"调度队列_{index+1}" in Config.running_list:
|
||||
logger.warning("相关调度队列正在运行")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
self.queue_manager.clear_SettingBox()
|
||||
|
||||
(Config.app_path / f"config/QueueConfig/调度队列_{index}.json").rename(
|
||||
Config.app_path / f"config/QueueConfig/调度队列_0.json",
|
||||
)
|
||||
(Config.app_path / f"config/QueueConfig/调度队列_{index+1}.json").rename(
|
||||
Config.app_path / f"config/QueueConfig/调度队列_{index}.json",
|
||||
)
|
||||
(Config.app_path / f"config/QueueConfig/调度队列_0.json").rename(
|
||||
Config.app_path / f"config/QueueConfig/调度队列_{index+1}.json",
|
||||
)
|
||||
|
||||
self.queue_manager.show_SettingBox(index + 1)
|
||||
|
||||
def refresh(self):
|
||||
"""刷新调度队列界面"""
|
||||
|
||||
if len(self.queue_manager.search_queue()) == 0:
|
||||
index = 0
|
||||
else:
|
||||
index = int(self.queue_manager.pivot.currentRouteKey()[5:])
|
||||
self.queue_manager.clear_SettingBox()
|
||||
self.queue_manager.show_SettingBox(index)
|
||||
|
||||
|
||||
class QueueSettingBox(QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("调度队列管理")
|
||||
|
||||
self.pivot = Pivot(self)
|
||||
self.stackedWidget = QStackedWidget(self)
|
||||
self.Layout = QVBoxLayout(self)
|
||||
|
||||
self.script_list: List[QueueMemberSettingBox] = []
|
||||
|
||||
self.Layout.addWidget(self.pivot, 0, Qt.AlignHCenter)
|
||||
self.Layout.addWidget(self.stackedWidget)
|
||||
self.Layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.pivot.currentItemChanged.connect(
|
||||
lambda index: self.switch_SettingBox(int(index[5:]), if_change_pivot=False)
|
||||
)
|
||||
|
||||
self.show_SettingBox(1)
|
||||
|
||||
def show_SettingBox(self, index) -> None:
|
||||
"""加载所有子界面"""
|
||||
|
||||
queue_list = self.search_queue()
|
||||
|
||||
qconfig.load(
|
||||
Config.app_path / "config/临时.json",
|
||||
Config.queue_config,
|
||||
)
|
||||
Config.clear_queue_config()
|
||||
for queue in queue_list:
|
||||
self.add_QueueSettingBox(int(queue[0][5:]))
|
||||
if (Config.app_path / "config/临时.json").exists():
|
||||
(Config.app_path / "config/临时.json").unlink()
|
||||
|
||||
self.switch_SettingBox(index)
|
||||
|
||||
def switch_SettingBox(self, index: int, if_change_pivot: bool = True) -> None:
|
||||
"""切换到指定的子界面"""
|
||||
|
||||
queue_list = self.search_queue()
|
||||
|
||||
if len(queue_list) == 0:
|
||||
return None
|
||||
|
||||
if index > len(queue_list):
|
||||
return None
|
||||
|
||||
qconfig.load(
|
||||
Config.app_path
|
||||
/ f"config/QueueConfig/{self.script_list[index-1].objectName()}.json",
|
||||
Config.queue_config,
|
||||
)
|
||||
|
||||
if if_change_pivot:
|
||||
self.pivot.setCurrentItem(self.script_list[index - 1].objectName())
|
||||
self.stackedWidget.setCurrentWidget(self.script_list[index - 1])
|
||||
|
||||
def clear_SettingBox(self) -> None:
|
||||
"""清空所有子界面"""
|
||||
|
||||
for sub_interface in self.script_list:
|
||||
self.stackedWidget.removeWidget(sub_interface)
|
||||
sub_interface.deleteLater()
|
||||
self.script_list.clear()
|
||||
self.pivot.clear()
|
||||
qconfig.load(
|
||||
Config.app_path / "config/临时.json",
|
||||
Config.queue_config,
|
||||
)
|
||||
Config.clear_queue_config()
|
||||
if (Config.app_path / "config/临时.json").exists():
|
||||
(Config.app_path / "config/临时.json").unlink()
|
||||
|
||||
def add_QueueSettingBox(self, uid: int) -> None:
|
||||
"""添加一个调度队列设置界面"""
|
||||
|
||||
maa_setting_box = QueueMemberSettingBox(uid, self)
|
||||
|
||||
self.script_list.append(maa_setting_box)
|
||||
|
||||
self.stackedWidget.addWidget(self.script_list[-1])
|
||||
|
||||
self.pivot.addItem(routeKey=f"调度队列_{uid}", text=f"调度队列 {uid}")
|
||||
|
||||
def search_queue(self) -> list:
|
||||
"""搜索所有调度队列实例"""
|
||||
|
||||
queue_list = []
|
||||
|
||||
if (Config.app_path / "config/QueueConfig").exists():
|
||||
for json_file in (Config.app_path / "config/QueueConfig").glob("*.json"):
|
||||
with json_file.open("r", encoding="utf-8") as f:
|
||||
info = json.load(f)
|
||||
queue_list.append([json_file.stem, info["QueueSet"]["Name"]])
|
||||
|
||||
return queue_list
|
||||
|
||||
|
||||
class QueueMemberSettingBox(QWidget):
|
||||
|
||||
def __init__(self, uid: int, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName(f"调度队列_{uid}")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
scrollArea = ScrollArea()
|
||||
scrollArea.setWidgetResizable(True)
|
||||
|
||||
content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(content_widget)
|
||||
|
||||
self.queue_set = self.QueueSetSettingCard(self)
|
||||
self.time = self.TimeSettingCard(self)
|
||||
self.task = self.TaskSettingCard(self)
|
||||
self.history = self.HistoryCard(self, f"调度队列_{uid}")
|
||||
|
||||
content_layout.addWidget(self.queue_set)
|
||||
content_layout.addWidget(self.time)
|
||||
content_layout.addWidget(self.task)
|
||||
content_layout.addWidget(self.history)
|
||||
content_layout.addStretch(1)
|
||||
|
||||
scrollArea.setWidget(content_widget)
|
||||
|
||||
layout.addWidget(scrollArea)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
class QueueSetSettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("队列设置")
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
|
||||
self.card_Name = LineEditSettingCard(
|
||||
"请输入调度队列名称",
|
||||
FluentIcon.EDIT,
|
||||
"调度队列名称",
|
||||
"用于标识调度队列的名称",
|
||||
Config.queue_config.queueSet_Name,
|
||||
)
|
||||
self.card_Enable = SwitchSettingCard(
|
||||
FluentIcon.HOME,
|
||||
"状态",
|
||||
"调度队列状态",
|
||||
Config.queue_config.queueSet_Enabled,
|
||||
)
|
||||
self.card_AfterAccomplish = ComboBoxSettingCard(
|
||||
configItem=Config.queue_config.queueSet_AfterAccomplish,
|
||||
icon=FluentIcon.POWER_BUTTON,
|
||||
title="调度队列结束后",
|
||||
content="选择调度队列结束后的操作",
|
||||
texts=[
|
||||
"无动作",
|
||||
"退出AUTO_MAA",
|
||||
"睡眠(win系统需禁用休眠)",
|
||||
"休眠",
|
||||
"关机",
|
||||
],
|
||||
)
|
||||
|
||||
Layout.addWidget(self.card_Name)
|
||||
Layout.addWidget(self.card_Enable)
|
||||
Layout.addWidget(self.card_AfterAccomplish)
|
||||
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
class TimeSettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("定时设置")
|
||||
|
||||
widget_1 = QWidget()
|
||||
Layout_1 = QVBoxLayout(widget_1)
|
||||
widget_2 = QWidget()
|
||||
Layout_2 = QVBoxLayout(widget_2)
|
||||
Layout = QHBoxLayout()
|
||||
|
||||
self.card_Time_0 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 1",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_0,
|
||||
Config.queue_config.time_TimeSet_0,
|
||||
)
|
||||
self.card_Time_1 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 2",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_1,
|
||||
Config.queue_config.time_TimeSet_1,
|
||||
)
|
||||
self.card_Time_2 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 3",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_2,
|
||||
Config.queue_config.time_TimeSet_2,
|
||||
)
|
||||
self.card_Time_3 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 4",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_3,
|
||||
Config.queue_config.time_TimeSet_3,
|
||||
)
|
||||
self.card_Time_4 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 5",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_4,
|
||||
Config.queue_config.time_TimeSet_4,
|
||||
)
|
||||
self.card_Time_5 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 6",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_5,
|
||||
Config.queue_config.time_TimeSet_5,
|
||||
)
|
||||
self.card_Time_6 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 7",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_6,
|
||||
Config.queue_config.time_TimeSet_6,
|
||||
)
|
||||
self.card_Time_7 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 8",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_7,
|
||||
Config.queue_config.time_TimeSet_7,
|
||||
)
|
||||
self.card_Time_8 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 9",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_8,
|
||||
Config.queue_config.time_TimeSet_8,
|
||||
)
|
||||
self.card_Time_9 = TimeEditSettingCard(
|
||||
FluentIcon.STOP_WATCH,
|
||||
"定时 10",
|
||||
"",
|
||||
Config.queue_config.time_TimeEnabled_9,
|
||||
Config.queue_config.time_TimeSet_9,
|
||||
)
|
||||
|
||||
Layout_1.addWidget(self.card_Time_0)
|
||||
Layout_1.addWidget(self.card_Time_1)
|
||||
Layout_1.addWidget(self.card_Time_2)
|
||||
Layout_1.addWidget(self.card_Time_3)
|
||||
Layout_1.addWidget(self.card_Time_4)
|
||||
Layout_2.addWidget(self.card_Time_5)
|
||||
Layout_2.addWidget(self.card_Time_6)
|
||||
Layout_2.addWidget(self.card_Time_7)
|
||||
Layout_2.addWidget(self.card_Time_8)
|
||||
Layout_2.addWidget(self.card_Time_9)
|
||||
Layout.addWidget(widget_1)
|
||||
Layout.addWidget(widget_2)
|
||||
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
class TaskSettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("任务队列")
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
|
||||
member_list = self.search_member()
|
||||
|
||||
self.card_Member_1 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_1,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 1",
|
||||
"第一个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
self.card_Member_2 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_2,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 2",
|
||||
"第二个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
self.card_Member_3 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_3,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 3",
|
||||
"第三个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
self.card_Member_4 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_4,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 4",
|
||||
"第四个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
self.card_Member_5 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_5,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 5",
|
||||
"第五个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
self.card_Member_6 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_6,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 6",
|
||||
"第六个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
self.card_Member_7 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_7,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 7",
|
||||
"第七个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
self.card_Member_8 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_8,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 8",
|
||||
"第八个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
self.card_Member_9 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_9,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 9",
|
||||
"第九个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
self.card_Member_10 = NoOptionComboBoxSettingCard(
|
||||
Config.queue_config.queue_Member_10,
|
||||
FluentIcon.APPLICATION,
|
||||
"任务实例 10",
|
||||
"第十个调起的脚本任务实例",
|
||||
member_list[0],
|
||||
member_list[1],
|
||||
)
|
||||
|
||||
Layout.addWidget(self.card_Member_1)
|
||||
Layout.addWidget(self.card_Member_2)
|
||||
Layout.addWidget(self.card_Member_3)
|
||||
Layout.addWidget(self.card_Member_4)
|
||||
Layout.addWidget(self.card_Member_5)
|
||||
Layout.addWidget(self.card_Member_6)
|
||||
Layout.addWidget(self.card_Member_7)
|
||||
Layout.addWidget(self.card_Member_8)
|
||||
Layout.addWidget(self.card_Member_9)
|
||||
Layout.addWidget(self.card_Member_10)
|
||||
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
def search_member(self) -> list:
|
||||
"""搜索所有脚本实例"""
|
||||
|
||||
member_list_name = ["禁用"]
|
||||
member_list_text = ["未启用"]
|
||||
|
||||
if (Config.app_path / "config/MaaConfig").exists():
|
||||
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
|
||||
if subdir.is_dir():
|
||||
member_list_name.append(subdir.name)
|
||||
with (subdir / "config.json").open("r", encoding="utf-8") as f:
|
||||
info = json.load(f)
|
||||
if info["MaaSet"]["Name"] != "":
|
||||
member_list_text.append(
|
||||
f"{subdir.name} - {info["MaaSet"]["Name"]}"
|
||||
)
|
||||
else:
|
||||
member_list_text.append(subdir.name)
|
||||
|
||||
return [member_list_name, member_list_text]
|
||||
|
||||
class HistoryCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None, name: str = None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("历史运行记录")
|
||||
|
||||
self.text = TextBrowser()
|
||||
self.text.setMinimumHeight(300)
|
||||
history = Config.get_history(name)
|
||||
self.text.setPlainText(history["History"])
|
||||
|
||||
self.viewLayout.addWidget(self.text)
|
||||
@@ -1,810 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA设置界面
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QApplication,
|
||||
QVBoxLayout,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
ScrollArea,
|
||||
FluentIcon,
|
||||
MessageBox,
|
||||
Dialog,
|
||||
HyperlinkCard,
|
||||
HeaderCardWidget,
|
||||
SwitchSettingCard,
|
||||
ExpandGroupSettingCard,
|
||||
PushSettingCard,
|
||||
ComboBoxSettingCard,
|
||||
)
|
||||
from datetime import datetime
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
import requests
|
||||
|
||||
from app.core import Config, MainInfoBar
|
||||
from app.services import Crypto, System
|
||||
from app.utils import Updater
|
||||
from .Widget import LineEditMessageBox, LineEditSettingCard, PasswordLineEditSettingCard
|
||||
|
||||
|
||||
class Setting(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("设置")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
scrollArea = ScrollArea()
|
||||
scrollArea.setWidgetResizable(True)
|
||||
|
||||
content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(content_widget)
|
||||
|
||||
self.function = FunctionSettingCard(self)
|
||||
self.start = StartSettingCard(self)
|
||||
self.ui = UiSettingCard(self)
|
||||
self.notification = NotifySettingCard(self)
|
||||
self.security = SecuritySettingCard(self)
|
||||
self.updater = UpdaterSettingCard(self)
|
||||
self.other = OtherSettingCard(self)
|
||||
|
||||
self.function.card_IfAllowSleep.checkedChanged.connect(System.set_Sleep)
|
||||
self.function.card_IfAgreeBilibili.checkedChanged.connect(self.agree_bilibili)
|
||||
self.start.card_IfSelfStart.checkedChanged.connect(System.set_SelfStart)
|
||||
self.security.card_changePASSWORD.clicked.connect(self.change_PASSWORD)
|
||||
self.updater.card_CheckUpdate.clicked.connect(self.get_update)
|
||||
self.other.card_Notice.clicked.connect(self.show_notice)
|
||||
|
||||
content_layout.addWidget(self.function)
|
||||
content_layout.addWidget(self.start)
|
||||
content_layout.addWidget(self.ui)
|
||||
content_layout.addWidget(self.notification)
|
||||
content_layout.addWidget(self.security)
|
||||
content_layout.addWidget(self.updater)
|
||||
content_layout.addWidget(self.other)
|
||||
|
||||
scrollArea.setWidget(content_widget)
|
||||
|
||||
layout.addWidget(scrollArea)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def agree_bilibili(self) -> None:
|
||||
"""授权bilibili游戏隐私政策"""
|
||||
|
||||
if not Config.global_config.get(Config.global_config.function_IfAgreeBilibili):
|
||||
logger.info("取消授权bilibili游戏隐私政策")
|
||||
MainInfoBar.push_info_bar(
|
||||
"info", "操作成功", "已取消授权bilibili游戏隐私政策", 3000
|
||||
)
|
||||
return None
|
||||
|
||||
choice = MessageBox(
|
||||
"授权声明",
|
||||
"开启“托管bilibili游戏隐私政策”功能,即代表您已完整阅读并同意《哔哩哔哩弹幕网用户使用协议》、《哔哩哔哩隐私政策》和《哔哩哔哩游戏中心用户协议》,并授权AUTO_MAA在其认定需要时以其认定合适的方法替您处理相关弹窗\n\n是否同意授权?",
|
||||
self.window(),
|
||||
)
|
||||
if choice.exec():
|
||||
logger.success("确认授权bilibili游戏隐私政策")
|
||||
MainInfoBar.push_info_bar(
|
||||
"success", "操作成功", "已确认授权bilibili游戏隐私政策", 3000
|
||||
)
|
||||
else:
|
||||
Config.global_config.set(
|
||||
Config.global_config.function_IfAgreeBilibili, False
|
||||
)
|
||||
|
||||
def check_PASSWORD(self) -> None:
|
||||
"""检查并配置管理密钥"""
|
||||
|
||||
if Config.key_path.exists():
|
||||
return None
|
||||
|
||||
while True:
|
||||
|
||||
choice = LineEditMessageBox(
|
||||
self.window(),
|
||||
"未检测到管理密钥,请设置您的管理密钥",
|
||||
"管理密钥",
|
||||
"密码",
|
||||
)
|
||||
if choice.exec() and choice.input.text() != "":
|
||||
Crypto.get_PASSWORD(choice.input.text())
|
||||
break
|
||||
else:
|
||||
choice = MessageBox(
|
||||
"警告",
|
||||
"您没有设置管理密钥,无法使用本软件,请先设置管理密钥",
|
||||
self.window(),
|
||||
)
|
||||
choice.cancelButton.hide()
|
||||
choice.buttonLayout.insertStretch(1)
|
||||
choice.exec()
|
||||
|
||||
def change_PASSWORD(self) -> None:
|
||||
"""修改管理密钥"""
|
||||
|
||||
if_change = True
|
||||
|
||||
while if_change:
|
||||
|
||||
choice = LineEditMessageBox(
|
||||
self.window(),
|
||||
"请输入旧的管理密钥",
|
||||
"旧管理密钥",
|
||||
"密码",
|
||||
)
|
||||
if choice.exec() and choice.input.text() != "":
|
||||
|
||||
# 验证旧管理密钥
|
||||
if Crypto.check_PASSWORD(choice.input.text()):
|
||||
|
||||
PASSWORD_old = choice.input.text()
|
||||
# 获取新的管理密钥
|
||||
while True:
|
||||
|
||||
choice = LineEditMessageBox(
|
||||
self.window(),
|
||||
"请输入新的管理密钥",
|
||||
"新管理密钥",
|
||||
"密码",
|
||||
)
|
||||
if choice.exec() and choice.input.text() != "":
|
||||
|
||||
# 修改管理密钥
|
||||
Crypto.change_PASSWORD(PASSWORD_old, choice.input.text())
|
||||
MainInfoBar.push_info_bar(
|
||||
"success", "操作成功", "管理密钥修改成功", 3000
|
||||
)
|
||||
if_change = False
|
||||
break
|
||||
|
||||
else:
|
||||
|
||||
choice = MessageBox(
|
||||
"确认",
|
||||
"您没有输入新的管理密钥,是否取消修改管理密钥?",
|
||||
self.window(),
|
||||
)
|
||||
if choice.exec():
|
||||
if_change = False
|
||||
break
|
||||
|
||||
else:
|
||||
choice = MessageBox("错误", "管理密钥错误", self.window())
|
||||
choice.cancelButton.hide()
|
||||
choice.buttonLayout.insertStretch(1)
|
||||
choice.exec()
|
||||
else:
|
||||
choice = MessageBox(
|
||||
"确认",
|
||||
"您没有输入管理密钥,是否取消修改管理密钥?",
|
||||
self.window(),
|
||||
)
|
||||
if choice.exec():
|
||||
break
|
||||
|
||||
def get_update_info(self) -> str:
|
||||
"""检查主程序版本更新,返回更新信息"""
|
||||
|
||||
# 从本地版本信息文件获取当前版本信息
|
||||
with Config.version_path.open(mode="r", encoding="utf-8") as f:
|
||||
version_current = json.load(f)
|
||||
main_version_current = list(
|
||||
map(int, version_current["main_version"].split("."))
|
||||
)
|
||||
|
||||
# 从远程服务器获取最新版本信息
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = requests.get(
|
||||
f"https://gitee.com/DLmaster_361/AUTO_MAA/raw/{Config.global_config.get(Config.global_config.update_UpdateType)}/resources/version.json"
|
||||
)
|
||||
version_remote = response.json()
|
||||
break
|
||||
except Exception as e:
|
||||
err = e
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
return f"获取版本信息时出错:\n{err}"
|
||||
|
||||
main_version_remote = list(map(int, version_remote["main_version"].split(".")))
|
||||
|
||||
# 有版本更新
|
||||
if main_version_remote > main_version_current:
|
||||
|
||||
main_version_info = f" 主程序:{version_text(main_version_current)} --> {version_text(main_version_remote)}\n"
|
||||
|
||||
return f"发现新版本:\n{main_version_info} 更新说明:\n{version_remote['announcement'].replace("\n# ","\n !").replace("\n## ","\n - ").replace("\n- ","\n · ")}\n\n是否开始更新?\n\n 注意:主程序更新时AUTO_MAA将自动关闭"
|
||||
|
||||
else:
|
||||
return "已是最新版本~"
|
||||
|
||||
def get_update(self, if_question: bool = True) -> None:
|
||||
"""检查版本更新,调起文件下载进程"""
|
||||
|
||||
# 从本地版本信息文件获取当前版本信息
|
||||
with Config.version_path.open(mode="r", encoding="utf-8") as f:
|
||||
version_current = json.load(f)
|
||||
main_version_current = list(
|
||||
map(int, version_current["main_version"].split("."))
|
||||
)
|
||||
updater_version_current = list(
|
||||
map(int, version_current["updater_version"].split("."))
|
||||
)
|
||||
# 检查更新器是否存在
|
||||
if not (Config.app_path / "Updater.exe").exists():
|
||||
updater_version_current = [0, 0, 0, 0]
|
||||
|
||||
# 从远程服务器获取最新版本信息
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = requests.get(
|
||||
f"https://gitee.com/DLmaster_361/AUTO_MAA/raw/{Config.global_config.get(Config.global_config.update_UpdateType)}/resources/version.json"
|
||||
)
|
||||
version_remote = response.json()
|
||||
break
|
||||
except Exception as e:
|
||||
err = e
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
choice = MessageBox(
|
||||
"错误",
|
||||
f"获取版本信息时出错:\n{err}",
|
||||
self.window(),
|
||||
)
|
||||
choice.cancelButton.hide()
|
||||
choice.buttonLayout.insertStretch(1)
|
||||
if choice.exec():
|
||||
return None
|
||||
|
||||
main_version_remote = list(map(int, version_remote["main_version"].split(".")))
|
||||
updater_version_remote = list(
|
||||
map(int, version_remote["updater_version"].split("."))
|
||||
)
|
||||
|
||||
# 有版本更新
|
||||
if (main_version_remote > main_version_current) or (
|
||||
updater_version_remote > updater_version_current
|
||||
):
|
||||
|
||||
# 生成版本更新信息
|
||||
if main_version_remote > main_version_current:
|
||||
main_version_info = f" 主程序:{version_text(main_version_current)} --> {version_text(main_version_remote)}\n"
|
||||
else:
|
||||
main_version_info = (
|
||||
f" 主程序:{version_text(main_version_current)}\n"
|
||||
)
|
||||
if updater_version_remote > updater_version_current:
|
||||
updater_version_info = f" 更新器:{version_text(updater_version_current)} --> {version_text(updater_version_remote)}\n"
|
||||
else:
|
||||
updater_version_info = (
|
||||
f" 更新器:{version_text(updater_version_current)}\n"
|
||||
)
|
||||
|
||||
# 询问是否开始版本更新
|
||||
if if_question:
|
||||
choice = MessageBox(
|
||||
"版本更新",
|
||||
f"发现新版本:\n{main_version_info}{updater_version_info} 更新说明:\n{version_remote['announcement'].replace("\n# ","\n !").replace("\n## ","\n - ").replace("\n- ","\n · ")}\n\n是否开始更新?\n\n 注意:主程序更新时AUTO_MAA将自动关闭",
|
||||
self.window(),
|
||||
)
|
||||
if not choice.exec():
|
||||
return None
|
||||
|
||||
# 更新更新器
|
||||
if updater_version_remote > updater_version_current:
|
||||
# 创建更新进程
|
||||
self.updater = Updater(
|
||||
Config.app_path,
|
||||
"AUTO_MAA更新器",
|
||||
main_version_remote,
|
||||
updater_version_remote,
|
||||
)
|
||||
# 完成更新器的更新后更新主程序
|
||||
if main_version_remote > main_version_current:
|
||||
self.updater.update_process.accomplish.connect(self.update_main)
|
||||
# 显示更新页面
|
||||
self.updater.ui.show()
|
||||
|
||||
# 更新主程序
|
||||
elif main_version_remote > main_version_current:
|
||||
self.update_main()
|
||||
|
||||
# 无版本更新
|
||||
else:
|
||||
MainInfoBar.push_info_bar("success", "更新检查", "已是最新版本~", 3000)
|
||||
|
||||
def update_main(self) -> None:
|
||||
"""更新主程序"""
|
||||
|
||||
subprocess.Popen(
|
||||
str(Config.app_path / "Updater.exe"),
|
||||
shell=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
self.close()
|
||||
QApplication.quit()
|
||||
|
||||
def show_notice(self, if_show: bool = True):
|
||||
"""显示公告"""
|
||||
|
||||
# 从远程服务器获取最新公告
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/notice.json"
|
||||
)
|
||||
notice = response.json()
|
||||
break
|
||||
except Exception as e:
|
||||
err = e
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
logger.warning(f"获取最新公告时出错:\n{err}")
|
||||
if if_show:
|
||||
choice = Dialog(
|
||||
"网络错误",
|
||||
f"获取最新公告时出错:\n{err}",
|
||||
self,
|
||||
)
|
||||
choice.cancelButton.hide()
|
||||
choice.buttonLayout.insertStretch(1)
|
||||
choice.exec()
|
||||
return None
|
||||
|
||||
if (Config.app_path / "resources/notice.json").exists():
|
||||
with (Config.app_path / "resources/notice.json").open(
|
||||
mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
notice_local = json.load(f)
|
||||
time_local = datetime.strptime(notice_local["time"], "%Y-%m-%d %H:%M")
|
||||
else:
|
||||
time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M")
|
||||
|
||||
if if_show or (
|
||||
datetime.now() > datetime.strptime(notice["time"], "%Y-%m-%d %H:%M")
|
||||
and datetime.strptime(notice["time"], "%Y-%m-%d %H:%M") > time_local
|
||||
):
|
||||
|
||||
choice = Dialog("公告", notice["content"], self)
|
||||
choice.cancelButton.hide()
|
||||
choice.buttonLayout.insertStretch(1)
|
||||
if choice.exec():
|
||||
with (Config.app_path / "resources/notice.json").open(
|
||||
mode="w", encoding="utf-8"
|
||||
) as f:
|
||||
json.dump(notice, f, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
class FunctionSettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("功能")
|
||||
|
||||
self.card_IfAllowSleep = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="启动时阻止系统休眠",
|
||||
content="仅阻止电脑自动休眠,不会影响屏幕是否熄灭",
|
||||
configItem=Config.global_config.function_IfAllowSleep,
|
||||
)
|
||||
self.card_IfSilence = self.SilenceSettingCard(self)
|
||||
self.card_IfAgreeBilibili = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="托管bilibili游戏隐私政策",
|
||||
content="授权AUTO_MAA同意bilibili游戏隐私政策",
|
||||
configItem=Config.global_config.function_IfAgreeBilibili,
|
||||
)
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_IfAllowSleep)
|
||||
Layout.addWidget(self.card_IfSilence)
|
||||
Layout.addWidget(self.card_IfAgreeBilibili)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
class SilenceSettingCard(ExpandGroupSettingCard):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(
|
||||
FluentIcon.SETTING,
|
||||
"静默模式",
|
||||
"将各代理窗口置于后台运行,减少对前台的干扰",
|
||||
parent,
|
||||
)
|
||||
|
||||
self.card_IfSilence = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="静默模式",
|
||||
content="是否启用静默模式",
|
||||
configItem=Config.global_config.function_IfSilence,
|
||||
)
|
||||
self.card_BossKey = LineEditSettingCard(
|
||||
text="请输入安卓模拟器老板键",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="模拟器老板键",
|
||||
content="输入模拟器老板快捷键,以“+”分隔",
|
||||
configItem=Config.global_config.function_BossKey,
|
||||
)
|
||||
|
||||
widget = QWidget()
|
||||
Layout = QVBoxLayout(widget)
|
||||
Layout.addWidget(self.card_IfSilence)
|
||||
Layout.addWidget(self.card_BossKey)
|
||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.viewLayout.setSpacing(0)
|
||||
self.addGroupWidget(widget)
|
||||
|
||||
|
||||
class StartSettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("启动")
|
||||
|
||||
self.card_IfSelfStart = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="开机时自动启动",
|
||||
content="将AUTO_MAA添加到开机启动项",
|
||||
configItem=Config.global_config.start_IfSelfStart,
|
||||
)
|
||||
self.card_IfRunDirectly = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="启动后直接运行主任务",
|
||||
content="启动AUTO_MAA后自动运行自动代理任务,优先级:调度队列 1 > 脚本 1",
|
||||
configItem=Config.global_config.start_IfRunDirectly,
|
||||
)
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_IfSelfStart)
|
||||
Layout.addWidget(self.card_IfRunDirectly)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
|
||||
class UiSettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("界面")
|
||||
|
||||
self.card_IfShowTray = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="显示托盘图标",
|
||||
content="常态显示托盘图标",
|
||||
configItem=Config.global_config.ui_IfShowTray,
|
||||
)
|
||||
self.card_IfToTray = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="最小化到托盘",
|
||||
content="最小化时隐藏到托盘",
|
||||
configItem=Config.global_config.ui_IfToTray,
|
||||
)
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_IfShowTray)
|
||||
Layout.addWidget(self.card_IfToTray)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
|
||||
class NotifySettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("通知")
|
||||
|
||||
self.card_IfSendErrorOnly = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="仅推送异常信息",
|
||||
content="仅在任务出现异常时推送通知",
|
||||
configItem=Config.global_config.notify_IfSendErrorOnly,
|
||||
)
|
||||
self.card_IfPushPlyer = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="推送系统通知",
|
||||
content="推送系统级通知,不会在通知中心停留",
|
||||
configItem=Config.global_config.notify_IfPushPlyer,
|
||||
)
|
||||
self.card_SendMail = self.SendMailSettingCard(self)
|
||||
self.card_ServerChan = self.ServerChanSettingCard(self)
|
||||
self.card_CompanyWebhookBot = self.CompanyWechatPushSettingCard(self)
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_IfSendErrorOnly)
|
||||
Layout.addWidget(self.card_IfPushPlyer)
|
||||
Layout.addWidget(self.card_SendMail)
|
||||
Layout.addWidget(self.card_ServerChan)
|
||||
Layout.addWidget(self.card_CompanyWebhookBot)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
class SendMailSettingCard(ExpandGroupSettingCard):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(
|
||||
FluentIcon.SETTING,
|
||||
"推送邮件通知",
|
||||
"通过电子邮箱推送任务结果",
|
||||
parent,
|
||||
)
|
||||
|
||||
self.card_IfSendMail = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="推送邮件通知",
|
||||
content="是否启用邮件通知功能",
|
||||
configItem=Config.global_config.notify_IfSendMail,
|
||||
)
|
||||
self.card_SMTPServerAddress = LineEditSettingCard(
|
||||
text="请输入SMTP服务器地址",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="SMTP服务器地址",
|
||||
content="发信邮箱的SMTP服务器地址",
|
||||
configItem=Config.global_config.notify_SMTPServerAddress,
|
||||
)
|
||||
self.card_FromAddress = LineEditSettingCard(
|
||||
text="请输入发信邮箱地址",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="发信邮箱地址",
|
||||
content="发送通知的邮箱地址",
|
||||
configItem=Config.global_config.notify_FromAddress,
|
||||
)
|
||||
self.card_AuthorizationCode = PasswordLineEditSettingCard(
|
||||
text="请输入发信邮箱授权码",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="发信邮箱授权码",
|
||||
content="发送通知的邮箱授权码",
|
||||
configItem=Config.global_config.notify_AuthorizationCode,
|
||||
)
|
||||
self.card_ToAddress = LineEditSettingCard(
|
||||
text="请输入收信邮箱地址",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="收信邮箱地址",
|
||||
content="接收通知的邮箱地址",
|
||||
configItem=Config.global_config.notify_ToAddress,
|
||||
)
|
||||
|
||||
widget = QWidget()
|
||||
Layout = QVBoxLayout(widget)
|
||||
Layout.addWidget(self.card_IfSendMail)
|
||||
Layout.addWidget(self.card_SMTPServerAddress)
|
||||
Layout.addWidget(self.card_FromAddress)
|
||||
Layout.addWidget(self.card_AuthorizationCode)
|
||||
Layout.addWidget(self.card_ToAddress)
|
||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.viewLayout.setSpacing(0)
|
||||
self.addGroupWidget(widget)
|
||||
|
||||
class ServerChanSettingCard(ExpandGroupSettingCard):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(
|
||||
FluentIcon.SETTING,
|
||||
"推送ServerChan通知",
|
||||
"通过ServerChan通知推送任务结果",
|
||||
parent,
|
||||
)
|
||||
|
||||
self.card_IfServerChan = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="推送SeverChan通知",
|
||||
content="是否启用SeverChan通知功能",
|
||||
configItem=Config.global_config.notify_IfServerChan,
|
||||
)
|
||||
self.card_ServerChanKey = LineEditSettingCard(
|
||||
text="请输入SendKey",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="SendKey",
|
||||
content="Server酱的SendKey(SC3与SCT都可以)",
|
||||
configItem=Config.global_config.notify_ServerChanKey,
|
||||
)
|
||||
self.card_ServerChanChannel = LineEditSettingCard(
|
||||
text="请输入需要推送的Channel代码(SCT生效)",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="ServerChanChannel代码",
|
||||
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
|
||||
configItem=Config.global_config.notify_ServerChanChannel,
|
||||
)
|
||||
self.card_ServerChanTag = LineEditSettingCard(
|
||||
text="请输入加入推送的Tag(SC3生效)",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="Tag内容",
|
||||
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
|
||||
configItem=Config.global_config.notify_ServerChanTag,
|
||||
)
|
||||
|
||||
widget = QWidget()
|
||||
Layout = QVBoxLayout(widget)
|
||||
Layout.addWidget(self.card_IfServerChan)
|
||||
Layout.addWidget(self.card_ServerChanKey)
|
||||
Layout.addWidget(self.card_ServerChanChannel)
|
||||
Layout.addWidget(self.card_ServerChanTag)
|
||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.viewLayout.setSpacing(0)
|
||||
self.addGroupWidget(widget)
|
||||
|
||||
class CompanyWechatPushSettingCard(ExpandGroupSettingCard):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(
|
||||
FluentIcon.SETTING,
|
||||
"推送企业微信机器人通知",
|
||||
"通过企业微信机器人Webhook通知推送任务结果",
|
||||
parent,
|
||||
)
|
||||
|
||||
self.card_IfCompanyWechat = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="推送企业微信机器人通知",
|
||||
content="是否启用企业微信机器人通知功能",
|
||||
configItem=Config.global_config.notify_IfCompanyWebHookBot,
|
||||
)
|
||||
self.card_CompanyWebHookBotUrl = LineEditSettingCard(
|
||||
text="请输入Webhook的Url",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="WebhookUrl",
|
||||
content="企业微信群机器人的Webhook地址",
|
||||
configItem=Config.global_config.notify_CompanyWebHookBotUrl,
|
||||
)
|
||||
|
||||
widget = QWidget()
|
||||
Layout = QVBoxLayout(widget)
|
||||
Layout.addWidget(self.card_IfCompanyWechat)
|
||||
Layout.addWidget(self.card_CompanyWebHookBotUrl)
|
||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.viewLayout.setSpacing(0)
|
||||
self.addGroupWidget(widget)
|
||||
|
||||
|
||||
class SecuritySettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("安全")
|
||||
|
||||
self.card_changePASSWORD = PushSettingCard(
|
||||
text="修改",
|
||||
icon=FluentIcon.VPN,
|
||||
title="修改管理密钥",
|
||||
content="修改用于解密用户密码的管理密钥",
|
||||
)
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_changePASSWORD)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
|
||||
class UpdaterSettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("更新")
|
||||
|
||||
self.card_IfAutoUpdate = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="自动检查更新",
|
||||
content="将在启动时自动检查AUTO_MAA是否有新版本",
|
||||
configItem=Config.global_config.update_IfAutoUpdate,
|
||||
)
|
||||
self.card_UpdateType = ComboBoxSettingCard(
|
||||
configItem=Config.global_config.update_UpdateType,
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="版本更新类别",
|
||||
content="选择AUTO_MAA的更新类别",
|
||||
texts=["稳定版", "公测版"],
|
||||
)
|
||||
self.card_CheckUpdate = PushSettingCard(
|
||||
text="检查更新",
|
||||
icon=FluentIcon.UPDATE,
|
||||
title="获取最新版本",
|
||||
content="检查AUTO_MAA是否有新版本",
|
||||
)
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_IfAutoUpdate)
|
||||
Layout.addWidget(self.card_UpdateType)
|
||||
Layout.addWidget(self.card_CheckUpdate)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
|
||||
class OtherSettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("其他")
|
||||
|
||||
self.card_Notice = PushSettingCard(
|
||||
text="查看",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="公告",
|
||||
content="查看AUTO_MAA的最新公告",
|
||||
)
|
||||
self.card_UserDocs = HyperlinkCard(
|
||||
url="https://clozya.github.io/AUTOMAA_docs",
|
||||
text="访问",
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="AUTO_MAA官方文档站",
|
||||
content="访问AUTO_MAA的官方文档站,获取使用指南和项目相关信息",
|
||||
)
|
||||
self.card_Association = self.AssociationSettingCard()
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_Notice)
|
||||
Layout.addWidget(self.card_UserDocs)
|
||||
Layout.addWidget(self.card_Association)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
class AssociationSettingCard(ExpandGroupSettingCard):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(
|
||||
FluentIcon.SETTING,
|
||||
"AUTO_MAA官方社群",
|
||||
"加入AUTO_MAA官方社群,获取更多帮助",
|
||||
parent,
|
||||
)
|
||||
|
||||
self.card_GitHubRepository = HyperlinkCard(
|
||||
url="https://github.com/DLmaster361/AUTO_MAA",
|
||||
text="访问GitHub仓库",
|
||||
icon=FluentIcon.GITHUB,
|
||||
title="GitHub",
|
||||
content="查看AUTO_MAA的源代码,提交问题和建议,欢迎参与开发",
|
||||
)
|
||||
self.card_QQGroup = HyperlinkCard(
|
||||
url="https://qm.qq.com/q/bd9fISNoME",
|
||||
text="加入官方QQ交流群",
|
||||
icon=FluentIcon.CHAT,
|
||||
title="QQ群",
|
||||
content="与AUTO_MAA开发者和用户交流",
|
||||
)
|
||||
|
||||
widget = QWidget()
|
||||
Layout = QVBoxLayout(widget)
|
||||
Layout.addWidget(self.card_GitHubRepository)
|
||||
Layout.addWidget(self.card_QQGroup)
|
||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.viewLayout.setSpacing(0)
|
||||
self.addGroupWidget(widget)
|
||||
|
||||
|
||||
def version_text(version_numb: list) -> str:
|
||||
"""将版本号列表转为可读的文本信息"""
|
||||
|
||||
if version_numb[3] == 0:
|
||||
version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}"
|
||||
else:
|
||||
version = (
|
||||
f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}"
|
||||
)
|
||||
return version
|
||||
88
app/utils/ImageUtils.py
Normal file
88
app/utils/ImageUtils.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2025 ClozyA
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class ImageUtils:
|
||||
@staticmethod
|
||||
def get_base64_from_file(image_path):
|
||||
"""从本地文件读取并返回base64编码字符串"""
|
||||
with open(image_path, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
def calculate_md5_from_file(image_path):
|
||||
"""从本地文件读取并返回md5值(hex字符串)"""
|
||||
with open(image_path, "rb") as f:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def calculate_md5_from_base64(base64_content):
|
||||
"""从base64字符串计算md5"""
|
||||
image_data = base64.b64decode(base64_content)
|
||||
return hashlib.md5(image_data).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def compress_image_if_needed(image_path: Path, max_size_mb=2) -> Path:
|
||||
"""
|
||||
如果图片大于max_size_mb, 则压缩并覆盖原文件, 返回原始路径(Path对象)
|
||||
"""
|
||||
|
||||
RESAMPLE = Image.Resampling.LANCZOS # Pillow 9.1.0及以后
|
||||
|
||||
max_size = max_size_mb * 1024 * 1024
|
||||
if image_path.stat().st_size <= max_size:
|
||||
return image_path
|
||||
|
||||
img = Image.open(image_path)
|
||||
suffix = image_path.suffix.lower()
|
||||
quality = 90 if suffix in [".jpg", ".jpeg"] else None
|
||||
step = 5
|
||||
|
||||
if quality is not None:
|
||||
while True:
|
||||
img.save(image_path, quality=quality, optimize=True)
|
||||
if image_path.stat().st_size <= max_size or quality <= 10:
|
||||
break
|
||||
quality -= step
|
||||
elif suffix == ".png":
|
||||
width, height = img.size
|
||||
while True:
|
||||
img.save(image_path, optimize=True)
|
||||
if (
|
||||
image_path.stat().st_size <= max_size
|
||||
or width <= 200
|
||||
or height <= 200
|
||||
):
|
||||
break
|
||||
width = int(width * 0.95)
|
||||
height = int(height * 0.95)
|
||||
img = img.resize((width, height), RESAMPLE)
|
||||
else:
|
||||
raise ValueError("仅支持JPG/JPEG和PNG格式图片的压缩")
|
||||
|
||||
return image_path
|
||||
156
app/utils/LogMonitor.py
Normal file
156
app/utils/LogMonitor.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import asyncio
|
||||
import aiofiles
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional, List, Awaitable
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger("日志监控器")
|
||||
|
||||
TIME_FIELDS = {
|
||||
"%Y": "year",
|
||||
"%m": "month",
|
||||
"%d": "day",
|
||||
"%H": "hour",
|
||||
"%M": "minute",
|
||||
"%S": "second",
|
||||
"%f": "microsecond",
|
||||
}
|
||||
"""时间字段映射表"""
|
||||
|
||||
|
||||
def strptime(date_string: str, format: str, default_date: datetime) -> datetime:
|
||||
"""根据指定格式解析日期字符串"""
|
||||
|
||||
date = datetime.strptime(date_string, format)
|
||||
|
||||
# 构建参数字典
|
||||
datetime_kwargs = {}
|
||||
for format_code, field_name in TIME_FIELDS.items():
|
||||
if format_code in format:
|
||||
datetime_kwargs[field_name] = getattr(date, field_name)
|
||||
else:
|
||||
datetime_kwargs[field_name] = getattr(default_date, field_name)
|
||||
|
||||
return datetime(**datetime_kwargs)
|
||||
|
||||
|
||||
class LogMonitor:
|
||||
def __init__(
|
||||
self,
|
||||
time_stamp_range: tuple[int, int],
|
||||
time_format: str,
|
||||
callback: Callable[[List[str]], Awaitable[None]],
|
||||
encoding: str = "utf-8",
|
||||
):
|
||||
self.time_stamp_range = time_stamp_range
|
||||
self.time_format = time_format
|
||||
self.callback = callback
|
||||
self.encoding = encoding
|
||||
self.log_file_path: Optional[Path] = None
|
||||
self.log_start_time: datetime = datetime.now()
|
||||
self.last_callback_time: datetime = datetime.now()
|
||||
self.log_contents: List[str] = []
|
||||
self.task: Optional[asyncio.Task] = None
|
||||
self.__is_running = False
|
||||
|
||||
async def monitor_log(self):
|
||||
"""监控日志文件的主循环"""
|
||||
if self.log_file_path is None or not self.log_file_path.exists():
|
||||
raise ValueError("日志文件路径未设置或文件不存在")
|
||||
|
||||
logger.info(f"开始监控日志文件: {self.log_file_path}")
|
||||
|
||||
while self.__is_running:
|
||||
logger.debug("正在检查日志文件...")
|
||||
log_contents = []
|
||||
if_log_start = False
|
||||
|
||||
# 检查文件是否仍然存在
|
||||
if not self.log_file_path.exists():
|
||||
logger.warning(f"日志文件不存在: {self.log_file_path}")
|
||||
continue
|
||||
|
||||
# 尝试读取文件
|
||||
try:
|
||||
async with aiofiles.open(
|
||||
self.log_file_path, "r", encoding=self.encoding
|
||||
) as f:
|
||||
async for line in f:
|
||||
if not if_log_start:
|
||||
try:
|
||||
entry_time = strptime(
|
||||
line[
|
||||
self.time_stamp_range[
|
||||
0
|
||||
] : self.time_stamp_range[1]
|
||||
],
|
||||
self.time_format,
|
||||
self.last_callback_time,
|
||||
)
|
||||
if entry_time > self.log_start_time:
|
||||
if_log_start = True
|
||||
log_contents.append(line)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
else:
|
||||
log_contents.append(line)
|
||||
|
||||
except (FileNotFoundError, PermissionError) as e:
|
||||
logger.warning(f"文件访问错误: {e}")
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
except UnicodeDecodeError as e:
|
||||
logger.error(f"文件编码错误: {e}")
|
||||
await asyncio.sleep(10)
|
||||
continue
|
||||
|
||||
# 调用回调
|
||||
if (
|
||||
log_contents != self.log_contents
|
||||
or datetime.now() - self.last_callback_time > timedelta(minutes=1)
|
||||
):
|
||||
self.log_contents = log_contents
|
||||
self.last_callback_time = datetime.now()
|
||||
|
||||
# 安全调用回调函数
|
||||
try:
|
||||
await self.callback(log_contents)
|
||||
except Exception as e:
|
||||
logger.error(f"回调函数执行失败: {e}")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def start(self, log_file_path: Path, start_time: datetime) -> None:
|
||||
"""启动监控"""
|
||||
|
||||
if log_file_path.is_dir():
|
||||
raise ValueError(f"日志文件不能是目录: {log_file_path}")
|
||||
|
||||
if self.task is not None and not self.task.done():
|
||||
await self.stop()
|
||||
|
||||
self.__is_running = True
|
||||
self.log_contents = []
|
||||
self.log_file_path = log_file_path
|
||||
self.log_start_time = start_time
|
||||
self.task = asyncio.create_task(self.monitor_log())
|
||||
logger.info(f"日志监控已启动: {self.log_file_path}")
|
||||
|
||||
async def stop(self):
|
||||
"""停止监控"""
|
||||
|
||||
logger.info("请求取消日志监控任务")
|
||||
|
||||
if self.task is not None and not self.task.done():
|
||||
self.task.cancel()
|
||||
|
||||
try:
|
||||
await self.task
|
||||
except asyncio.CancelledError:
|
||||
logger.info("日志监控任务已中止")
|
||||
|
||||
logger.success("日志监控任务已停止")
|
||||
self.task = None
|
||||
156
app/utils/ProcessManager.py
Normal file
156
app/utils/ProcessManager.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import asyncio
|
||||
import psutil
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""进程监视器类, 用于跟踪主进程及其所有子进程的状态"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.main_pid = None
|
||||
self.tracked_pids = set()
|
||||
self.check_task = None
|
||||
self.track_end_time = datetime.now()
|
||||
|
||||
async def open_process(
|
||||
self, path: Path, args: list = [], tracking_time: int = 60
|
||||
) -> None:
|
||||
"""
|
||||
启动一个新进程并返回其pid, 并开始监视该进程
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path: 可执行文件的路径
|
||||
args: 启动参数列表
|
||||
tracking_time: 子进程追踪持续时间(秒)
|
||||
"""
|
||||
|
||||
process = subprocess.Popen(
|
||||
[path, *args],
|
||||
cwd=path.parent,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
await self.start_monitoring(process.pid, tracking_time)
|
||||
|
||||
async def start_monitoring(self, pid: int, tracking_time: int = 60) -> None:
|
||||
"""
|
||||
启动进程监视器, 跟踪指定的主进程及其子进程
|
||||
|
||||
:param pid: 被监视进程的PID
|
||||
:param tracking_time: 子进程追踪持续时间(秒)
|
||||
"""
|
||||
|
||||
await self.clear()
|
||||
|
||||
self.main_pid = pid
|
||||
self.tracking_time = tracking_time
|
||||
|
||||
# 扫描并记录所有相关进程
|
||||
try:
|
||||
# 获取主进程
|
||||
main_proc = psutil.Process(self.main_pid)
|
||||
self.tracked_pids.add(self.main_pid)
|
||||
|
||||
# 递归获取所有子进程
|
||||
if tracking_time:
|
||||
for child in main_proc.children(recursive=True):
|
||||
self.tracked_pids.add(child.pid)
|
||||
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
# 启动持续追踪任务
|
||||
if tracking_time > 0:
|
||||
self.track_end_time = datetime.now() + timedelta(seconds=tracking_time)
|
||||
self.check_task = asyncio.create_task(self.track_processes())
|
||||
|
||||
async def track_processes(self) -> None:
|
||||
"""更新子进程列表"""
|
||||
|
||||
while datetime.now() < self.track_end_time:
|
||||
current_pids = set(self.tracked_pids)
|
||||
for pid in current_pids:
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
for child in proc.children():
|
||||
if child.pid not in self.tracked_pids:
|
||||
# 新发现的子进程
|
||||
self.tracked_pids.add(child.pid)
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def is_running(self) -> bool:
|
||||
"""检查所有跟踪的进程是否还在运行"""
|
||||
|
||||
for pid in self.tracked_pids:
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
if proc.is_running():
|
||||
return True
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
async def kill(self, if_force: bool = False) -> None:
|
||||
"""停止监视器并中止所有跟踪的进程"""
|
||||
|
||||
for pid in self.tracked_pids:
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
if if_force:
|
||||
kill_process = subprocess.Popen(
|
||||
["taskkill", "/F", "/T", "/PID", str(pid)],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
kill_process.wait()
|
||||
proc.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
|
||||
await self.clear()
|
||||
|
||||
async def clear(self) -> None:
|
||||
"""清空跟踪的进程列表"""
|
||||
|
||||
if self.check_task is not None and not self.check_task.done():
|
||||
self.check_task.cancel()
|
||||
|
||||
try:
|
||||
await self.check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
self.main_pid = None
|
||||
self.tracked_pids.clear()
|
||||
@@ -1,400 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA更新器
|
||||
v1.1
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import zipfile
|
||||
import requests
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from qfluentwidgets import ProgressBar, IndeterminateProgressBar, BodyLabel
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtCore import QObject, QThread, Signal
|
||||
|
||||
|
||||
def version_text(version_numb: list) -> str:
|
||||
"""将版本号列表转为可读的文本信息"""
|
||||
|
||||
if version_numb[3] == 0:
|
||||
version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}"
|
||||
else:
|
||||
version = (
|
||||
f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}"
|
||||
)
|
||||
return version
|
||||
|
||||
|
||||
class UpdateProcess(QThread):
|
||||
|
||||
info = Signal(str)
|
||||
progress = Signal(int, int, int)
|
||||
accomplish = Signal()
|
||||
|
||||
def __init__(
|
||||
self, app_path: Path, name: str, main_version: list, updater_version: list
|
||||
) -> None:
|
||||
super(UpdateProcess, self).__init__()
|
||||
|
||||
self.app_path = app_path
|
||||
self.name = name
|
||||
self.main_version = main_version
|
||||
self.updater_version = updater_version
|
||||
self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径
|
||||
self.version_path = app_path / "resources/version.json"
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
# 清理可能存在的临时文件
|
||||
if self.download_path.exists():
|
||||
self.download_path.unlink()
|
||||
|
||||
self.info.emit("正在获取下载链接")
|
||||
url_list = self.get_download_url()
|
||||
|
||||
# 验证下载地址并获取文件大小
|
||||
for i in range(len(url_list)):
|
||||
try:
|
||||
self.info.emit(f"正在验证下载地址:{url_list[i]}")
|
||||
response = requests.get(url_list[i], stream=True)
|
||||
if response.status_code != 200:
|
||||
self.info.emit(
|
||||
f"连接失败,错误代码 {response.status_code} ,正在切换代理({i+1}/{len(url_list)})"
|
||||
)
|
||||
time.sleep(1)
|
||||
continue
|
||||
file_size = response.headers.get("Content-Length")
|
||||
break
|
||||
except requests.RequestException:
|
||||
self.info.emit(f"请求超时,正在切换代理({i+1}/{len(url_list)})")
|
||||
time.sleep(1)
|
||||
else:
|
||||
self.info.emit(f"连接失败,已尝试所有{len(url_list)}个代理")
|
||||
return None
|
||||
|
||||
if file_size is None:
|
||||
file_size = 1
|
||||
else:
|
||||
file_size = int(file_size)
|
||||
|
||||
try:
|
||||
# 下载文件
|
||||
with open(self.download_path, "wb") as f:
|
||||
|
||||
downloaded_size = 0
|
||||
last_download_size = 0
|
||||
speed = 0
|
||||
last_time = time.time()
|
||||
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
|
||||
# 写入已下载数据
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# 计算下载速度
|
||||
if time.time() - last_time >= 1.0:
|
||||
speed = (
|
||||
(downloaded_size - last_download_size)
|
||||
/ (time.time() - last_time)
|
||||
/ 1024
|
||||
)
|
||||
last_download_size = downloaded_size
|
||||
last_time = time.time()
|
||||
|
||||
# 更新下载进度
|
||||
if speed >= 1024:
|
||||
self.info.emit(
|
||||
f"正在下载:{self.name} 已下载:{downloaded_size / 1048576:.2f}/{file_size / 1048576:.2f} MB ({downloaded_size / file_size * 100:.2f}%) 下载速度:{speed / 1024:.2f} MB/s",
|
||||
)
|
||||
else:
|
||||
self.info.emit(
|
||||
f"正在下载:{self.name} 已下载:{downloaded_size / 1048576:.2f}/{file_size / 1048576:.2f} MB ({downloaded_size / file_size * 100:.2f}%) 下载速度:{speed:.2f} KB/s",
|
||||
)
|
||||
self.progress.emit(0, 100, int(downloaded_size / file_size * 100))
|
||||
|
||||
except Exception as e:
|
||||
e = str(e)
|
||||
e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)])
|
||||
self.info.emit(f"下载{self.name}时出错:\n{e}")
|
||||
return None
|
||||
|
||||
# 解压
|
||||
try:
|
||||
|
||||
while True:
|
||||
try:
|
||||
self.info.emit("正在解压更新文件")
|
||||
self.progress.emit(0, 0, 0)
|
||||
with zipfile.ZipFile(self.download_path, "r") as zip_ref:
|
||||
zip_ref.extractall(self.app_path)
|
||||
break
|
||||
except PermissionError:
|
||||
self.info.emit(f"解压出错:{self.name}正在运行,正在等待其关闭")
|
||||
time.sleep(1)
|
||||
|
||||
self.info.emit("正在删除临时文件")
|
||||
self.progress.emit(0, 0, 0)
|
||||
self.download_path.unlink()
|
||||
|
||||
self.info.emit(f"{self.name}更新成功!")
|
||||
self.progress.emit(0, 100, 100)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
e = str(e)
|
||||
e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)])
|
||||
self.info.emit(f"解压更新时出错:\n{e}")
|
||||
return None
|
||||
|
||||
# 更新version文件
|
||||
if self.name in ["AUTO_MAA主程序", "AUTO_MAA更新器"]:
|
||||
with open(self.version_path, "r", encoding="utf-8") as f:
|
||||
version_info = json.load(f)
|
||||
if self.name == "AUTO_MAA主程序":
|
||||
version_info["main_version"] = ".".join(map(str, self.main_version))
|
||||
elif self.name == "AUTO_MAA更新器":
|
||||
version_info["updater_version"] = ".".join(
|
||||
map(str, self.updater_version)
|
||||
)
|
||||
with open(self.version_path, "w", encoding="utf-8") as f:
|
||||
json.dump(version_info, f, ensure_ascii=False, indent=4)
|
||||
|
||||
# 主程序更新完成后打开AUTO_MAA
|
||||
if self.name == "AUTO_MAA主程序":
|
||||
subprocess.Popen(
|
||||
str(self.app_path / "AUTO_MAA.exe"),
|
||||
shell=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
elif self.name == "MAA":
|
||||
subprocess.Popen(
|
||||
str(self.app_path / "MAA.exe"),
|
||||
shell=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
|
||||
self.accomplish.emit()
|
||||
|
||||
def get_download_url(self) -> list:
|
||||
"""获取下载链接"""
|
||||
|
||||
try_num = 3
|
||||
for i in range(try_num):
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://gitee.com/DLmaster_361/AUTO_MAA/raw/main/resources/version.json"
|
||||
)
|
||||
if response.status_code != 200:
|
||||
self.info.emit(
|
||||
f"连接失败,错误代码 {response.status_code} ,正在重试({i+1}/{try_num})"
|
||||
)
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
version_remote = response.json()
|
||||
PROXY_list = version_remote["proxy_list"]
|
||||
break
|
||||
except requests.RequestException:
|
||||
self.info.emit(f"请求超时,正在重试({i+1}/{try_num})")
|
||||
time.sleep(0.1)
|
||||
except KeyError:
|
||||
self.info.emit(f"未找到远端代理网址项,正在重试({i+1}/{try_num})")
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
self.info.emit("获取远端代理信息失败,将使用默认代理地址")
|
||||
PROXY_list = [
|
||||
"",
|
||||
"https://gitproxy.click/",
|
||||
"https://cdn.moran233.xyz/",
|
||||
"https://gh.llkk.cc/",
|
||||
"https://github.akams.cn/",
|
||||
"https://www.ghproxy.cn/",
|
||||
"https://ghfast.top/",
|
||||
]
|
||||
time.sleep(1)
|
||||
|
||||
url_list = []
|
||||
if self.name == "AUTO_MAA主程序":
|
||||
url_list.append(
|
||||
f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
|
||||
)
|
||||
url_list.append(
|
||||
f"https://jp-download.fearr.xyz/AUTO_MAA/AUTO_MAA_{version_text(self.main_version)}.zip"
|
||||
)
|
||||
for i in range(len(PROXY_list)):
|
||||
url_list.append(
|
||||
f"{PROXY_list[i]}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
|
||||
)
|
||||
elif self.name == "AUTO_MAA更新器":
|
||||
url_list.append(
|
||||
f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.main_version)}/Updater_{version_text(self.updater_version)}.zip"
|
||||
)
|
||||
url_list.append(
|
||||
f"https://jp-download.fearr.xyz/AUTO_MAA/Updater_{version_text(self.updater_version)}.zip"
|
||||
)
|
||||
for i in range(len(PROXY_list)):
|
||||
url_list.append(
|
||||
f"{PROXY_list[i]}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.main_version)}/Updater_{version_text(self.updater_version)}.zip"
|
||||
)
|
||||
elif self.name == "MAA":
|
||||
url_list.append(
|
||||
f"https://jp-download.fearr.xyz/MAA/MAA-{version_text(self.main_version)}-win-x64.zip"
|
||||
)
|
||||
for i in range(len(PROXY_list)):
|
||||
url_list.append(
|
||||
f"{PROXY_list[i]}https://github.com/MaaAssistantArknights/MaaAssistantArknights/releases/download/{version_text(self.main_version)}/MAA-{version_text(self.main_version)}-win-x64.zip"
|
||||
)
|
||||
return url_list
|
||||
|
||||
|
||||
class Updater(QObject):
|
||||
|
||||
def __init__(
|
||||
self, app_path: Path, name: str, main_version: list, updater_version: list
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.ui = QDialog()
|
||||
self.ui.setWindowTitle("AUTO_MAA更新器")
|
||||
self.ui.resize(700, 70)
|
||||
self.ui.setWindowIcon(
|
||||
QIcon(
|
||||
str(
|
||||
Path(sys.argv[0]).resolve().parent
|
||||
/ "resources/icons/AUTO_MAA_Updater.ico"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# 创建垂直布局
|
||||
self.Layout = QVBoxLayout(self.ui)
|
||||
|
||||
self.info = BodyLabel("正在初始化", self.ui)
|
||||
self.progress_1 = IndeterminateProgressBar(self.ui)
|
||||
self.progress_2 = ProgressBar(self.ui)
|
||||
|
||||
self.update_progress(0, 0, 0)
|
||||
|
||||
self.Layout.addWidget(self.info)
|
||||
self.Layout.addStretch(1)
|
||||
self.Layout.addWidget(self.progress_1)
|
||||
self.Layout.addWidget(self.progress_2)
|
||||
self.Layout.addStretch(1)
|
||||
|
||||
self.update_process = UpdateProcess(
|
||||
app_path, name, main_version, updater_version
|
||||
)
|
||||
|
||||
self.update_process.info.connect(self.update_info)
|
||||
self.update_process.progress.connect(self.update_progress)
|
||||
|
||||
self.update_process.start()
|
||||
|
||||
def update_info(self, text: str) -> None:
|
||||
self.info.setText(text)
|
||||
|
||||
def update_progress(self, begin: int, end: int, current: int) -> None:
|
||||
if begin == 0 and end == 0:
|
||||
self.progress_2.setVisible(False)
|
||||
self.progress_1.setVisible(True)
|
||||
else:
|
||||
self.progress_1.setVisible(False)
|
||||
self.progress_2.setVisible(True)
|
||||
self.progress_2.setRange(begin, end)
|
||||
self.progress_2.setValue(current)
|
||||
|
||||
|
||||
class AUTO_MAA_Updater(QApplication):
|
||||
def __init__(
|
||||
self, app_path: Path, name: str, main_version: list, updater_version: list
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.main = Updater(app_path, name, main_version, updater_version)
|
||||
self.main.ui.show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 获取软件自身的路径
|
||||
app_path = Path(sys.argv[0]).resolve().parent
|
||||
|
||||
# 从本地版本信息文件获取当前版本信息
|
||||
if (app_path / "resources/version.json").exists():
|
||||
with (app_path / "resources/version.json").open(
|
||||
mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
version_current = json.load(f)
|
||||
main_version_current = list(
|
||||
map(int, version_current["main_version"].split("."))
|
||||
)
|
||||
else:
|
||||
main_version_current = [0, 0, 0, 0]
|
||||
|
||||
# 从本地配置文件获取更新类型
|
||||
if (app_path / "config/config.json").exists():
|
||||
with (app_path / "config/config.json").open(mode="r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
if "Update" in config and "UpdateType" in config["Update"]:
|
||||
update_type = config["Update"]["UpdateType"]
|
||||
else:
|
||||
update_type = "main"
|
||||
else:
|
||||
update_type = "main"
|
||||
|
||||
# 从远程服务器获取最新版本信息
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = requests.get(
|
||||
f"https://gitee.com/DLmaster_361/AUTO_MAA/raw/{update_type}/resources/version.json"
|
||||
)
|
||||
version_remote = response.json()
|
||||
main_version_remote = list(
|
||||
map(int, version_remote["main_version"].split("."))
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
err = e
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
sys.exit(f"获取版本信息时出错:\n{err}")
|
||||
|
||||
# 启动更新线程
|
||||
if main_version_remote > main_version_current:
|
||||
app = AUTO_MAA_Updater(
|
||||
app_path,
|
||||
"AUTO_MAA主程序",
|
||||
main_version_remote,
|
||||
[],
|
||||
)
|
||||
sys.exit(app.exec())
|
||||
@@ -1,34 +1,44 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA工具包
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
__version__ = "4.2.0"
|
||||
__version__ = "5.0.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .Updater import Updater
|
||||
|
||||
__all__ = ["Updater"]
|
||||
from .constants import *
|
||||
from .logger import get_logger
|
||||
from .ImageUtils import ImageUtils
|
||||
from .LogMonitor import LogMonitor, strptime
|
||||
from .ProcessManager import ProcessManager
|
||||
from .security import dpapi_encrypt, dpapi_decrypt
|
||||
|
||||
__all__ = [
|
||||
"constants",
|
||||
"get_logger",
|
||||
"ImageUtils",
|
||||
"LogMonitor",
|
||||
"ProcessManager",
|
||||
"dpapi_encrypt",
|
||||
"dpapi_decrypt",
|
||||
"strptime",
|
||||
]
|
||||
|
||||
288
app/utils/constants.py
Normal file
288
app/utils/constants.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 ClozyA
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
RESOURCE_STAGE_INFO = [
|
||||
{"value": "-", "text": "当前/上次", "days": [1, 2, 3, 4, 5, 6, 7]},
|
||||
{"value": "1-7", "text": "1-7", "days": [1, 2, 3, 4, 5, 6, 7]},
|
||||
{"value": "R8-11", "text": "R8-11", "days": [1, 2, 3, 4, 5, 6, 7]},
|
||||
{"value": "12-17-HARD", "text": "12-17-HARD", "days": [1, 2, 3, 4, 5, 6, 7]},
|
||||
{"value": "LS-6", "text": "经验-6/5", "days": [1, 2, 3, 4, 5, 6, 7]},
|
||||
{"value": "CE-6", "text": "龙门币-6/5", "days": [2, 4, 6, 7]},
|
||||
{"value": "AP-5", "text": "红票-5", "days": [1, 4, 6, 7]},
|
||||
{"value": "CA-5", "text": "技能-5", "days": [2, 3, 5, 7]},
|
||||
{"value": "SK-5", "text": "碳-5", "days": [1, 3, 5, 6]},
|
||||
{"value": "PR-A-1", "text": "奶/盾芯片", "days": [1, 4, 5, 7]},
|
||||
{"value": "PR-A-2", "text": "奶/盾芯片组", "days": [1, 4, 5, 7]},
|
||||
{"value": "PR-B-1", "text": "术/狙芯片", "days": [1, 2, 5, 6]},
|
||||
{"value": "PR-B-2", "text": "术/狙芯片组", "days": [1, 2, 5, 6]},
|
||||
{"value": "PR-C-1", "text": "先/辅芯片", "days": [3, 4, 6, 7]},
|
||||
{"value": "PR-C-2", "text": "先/辅芯片组", "days": [3, 4, 6, 7]},
|
||||
{"value": "PR-D-1", "text": "近/特芯片", "days": [2, 3, 6, 7]},
|
||||
{"value": "PR-D-2", "text": "近/特芯片组", "days": [2, 3, 6, 7]},
|
||||
]
|
||||
"""常规资源关信息"""
|
||||
|
||||
|
||||
RESOURCE_STAGE_DATE_TEXT = {
|
||||
"LS-6": "经验-6/5 | 常驻开放",
|
||||
"CE-6": "龙门币-6/5 | 二四六日开放",
|
||||
"AP-5": "红票-5 | 一四六日开放",
|
||||
"CA-5": "技能-5 | 二三五日开放",
|
||||
"SK-5": "碳-5 | 一三五六开放",
|
||||
"PR-A-1": "奶/盾芯片 | 一四五日开放",
|
||||
"PR-A-2": "奶/盾芯片组 | 一四五日开放",
|
||||
"PR-B-1": "术/狙芯片 | 一二五六日开放",
|
||||
"PR-B-2": "术/狙芯片组 | 一二五六日开放",
|
||||
"PR-C-1": "先/辅芯片 | 三四六日开放",
|
||||
"PR-C-2": "先/辅芯片组 | 三四六日开放",
|
||||
"PR-D-1": "近/特芯片 | 二三六日开放",
|
||||
"PR-D-2": "近/特芯片组 | 二三六日开放",
|
||||
}
|
||||
"""常规资源关开放日文本映射"""
|
||||
|
||||
|
||||
RESOURCE_STAGE_DROP_INFO = {
|
||||
"CE-6": {
|
||||
"Display": "CE-6",
|
||||
"Value": "CE-6",
|
||||
"Drop": "4001",
|
||||
"DropName": "龙门币",
|
||||
"Activity": {"Tip": "二四六日", "StageName": "资源关卡"},
|
||||
},
|
||||
"AP-5": {
|
||||
"Display": "AP-5",
|
||||
"Value": "AP-5",
|
||||
"Drop": "4006",
|
||||
"DropName": "采购凭证",
|
||||
"Activity": {"Tip": "一四六日", "StageName": "资源关卡"},
|
||||
},
|
||||
"CA-5": {
|
||||
"Display": "CA-5",
|
||||
"Value": "CA-5",
|
||||
"Drop": "3303",
|
||||
"DropName": "技巧概要",
|
||||
"Activity": {"Tip": "二三五日", "StageName": "资源关卡"},
|
||||
},
|
||||
"LS-6": {
|
||||
"Display": "LS-6",
|
||||
"Value": "LS-6",
|
||||
"Drop": "2004",
|
||||
"DropName": "作战记录",
|
||||
"Activity": {"Tip": "常驻开放", "StageName": "资源关卡"},
|
||||
},
|
||||
"SK-5": {
|
||||
"Display": "SK-5",
|
||||
"Value": "SK-5",
|
||||
"Drop": "3114",
|
||||
"DropName": "碳素组",
|
||||
"Activity": {"Tip": "一三五六", "StageName": "资源关卡"},
|
||||
},
|
||||
"PR-A-1": {
|
||||
"Display": "PR-A",
|
||||
"Value": "PR-A",
|
||||
"Drop": "PR-A",
|
||||
"DropName": "奶/盾芯片",
|
||||
"Activity": {"Tip": "一四五日", "StageName": "资源关卡"},
|
||||
},
|
||||
"PR-B-1": {
|
||||
"Display": "PR-B",
|
||||
"Value": "PR-B",
|
||||
"Drop": "PR-B",
|
||||
"DropName": "术/狙芯片",
|
||||
"Activity": {"Tip": "一二五六", "StageName": "资源关卡"},
|
||||
},
|
||||
"PR-C-1": {
|
||||
"Display": "PR-C",
|
||||
"Value": "PR-C",
|
||||
"Drop": "PR-C",
|
||||
"DropName": "先/辅芯片",
|
||||
"Activity": {"Tip": "三四六日", "StageName": "资源关卡"},
|
||||
},
|
||||
"PR-D-1": {
|
||||
"Display": "PR-D",
|
||||
"Value": "PR-D",
|
||||
"Drop": "PR-D",
|
||||
"DropName": "近/特芯片",
|
||||
"Activity": {"Tip": "二三六日", "StageName": "资源关卡"},
|
||||
},
|
||||
}
|
||||
"""常规资源关掉落信息"""
|
||||
|
||||
MATERIALS_MAP = {
|
||||
"4001": "龙门币",
|
||||
"4006": "采购凭证",
|
||||
"2004": "高级作战记录",
|
||||
"2003": "中级作战记录",
|
||||
"2002": "初级作战记录",
|
||||
"2001": "基础作战记录",
|
||||
"3303": "技巧概要·卷3",
|
||||
"3302": "技巧概要·卷2",
|
||||
"3301": "技巧概要·卷1",
|
||||
"30165": "重相位对映体",
|
||||
"30155": "烧结核凝晶",
|
||||
"30145": "晶体电子单元",
|
||||
"30135": "D32钢",
|
||||
"30125": "双极纳米片",
|
||||
"30115": "聚合剂",
|
||||
"31094": "手性屈光体",
|
||||
"31093": "类凝结核",
|
||||
"31084": "环烃预制体",
|
||||
"31083": "环烃聚质",
|
||||
"31074": "固化纤维板",
|
||||
"31073": "褐素纤维",
|
||||
"31064": "转质盐聚块",
|
||||
"31063": "转质盐组",
|
||||
"31054": "切削原液",
|
||||
"31053": "化合切削液",
|
||||
"31044": "精炼溶剂",
|
||||
"31043": "半自然溶剂",
|
||||
"31034": "晶体电路",
|
||||
"31033": "晶体元件",
|
||||
"31024": "炽合金块",
|
||||
"31023": "炽合金",
|
||||
"31014": "聚合凝胶",
|
||||
"31013": "凝胶",
|
||||
"30074": "白马醇",
|
||||
"30073": "扭转醇",
|
||||
"30084": "三水锰矿",
|
||||
"30083": "轻锰矿",
|
||||
"30094": "五水研磨石",
|
||||
"30093": "研磨石",
|
||||
"30104": "RMA70-24",
|
||||
"30103": "RMA70-12",
|
||||
"30014": "提纯源岩",
|
||||
"30013": "固源岩组",
|
||||
"30012": "固源岩",
|
||||
"30011": "源岩",
|
||||
"30064": "改量装置",
|
||||
"30063": "全新装置",
|
||||
"30062": "装置",
|
||||
"30061": "破损装置",
|
||||
"30034": "聚酸酯块",
|
||||
"30033": "聚酸酯组",
|
||||
"30032": "聚酸酯",
|
||||
"30031": "酯原料",
|
||||
"30024": "糖聚块",
|
||||
"30023": "糖组",
|
||||
"30022": "糖",
|
||||
"30021": "代糖",
|
||||
"30044": "异铁块",
|
||||
"30043": "异铁组",
|
||||
"30042": "异铁",
|
||||
"30041": "异铁碎片",
|
||||
"30054": "酮阵列",
|
||||
"30053": "酮凝集组",
|
||||
"30052": "酮凝集",
|
||||
"30051": "双酮",
|
||||
"3114": "碳素组",
|
||||
"3113": "碳素",
|
||||
"3112": "碳",
|
||||
"3213": "先锋双芯片",
|
||||
"3223": "近卫双芯片",
|
||||
"3233": "重装双芯片",
|
||||
"3243": "狙击双芯片",
|
||||
"3253": "术师双芯片",
|
||||
"3263": "医疗双芯片",
|
||||
"3273": "辅助双芯片",
|
||||
"3283": "特种双芯片",
|
||||
"3212": "先锋芯片组",
|
||||
"3222": "近卫芯片组",
|
||||
"3232": "重装芯片组",
|
||||
"3242": "狙击芯片组",
|
||||
"3252": "术师芯片组",
|
||||
"3262": "医疗芯片组",
|
||||
"3272": "辅助芯片组",
|
||||
"3282": "特种芯片组",
|
||||
"3211": "先锋芯片",
|
||||
"3221": "近卫芯片",
|
||||
"3231": "重装芯片",
|
||||
"3241": "狙击芯片",
|
||||
"3251": "术师芯片",
|
||||
"3261": "医疗芯片",
|
||||
"3271": "辅助芯片",
|
||||
"3281": "特种芯片",
|
||||
"PR-A": "医疗/重装芯片",
|
||||
"PR-B": "术师/狙击芯片",
|
||||
"PR-C": "先锋/辅助芯片",
|
||||
"PR-D": "近卫/特种芯片",
|
||||
}
|
||||
"""掉落物索引表"""
|
||||
|
||||
POWER_SIGN_MAP = {
|
||||
"NoAction": "无动作",
|
||||
"Shutdown": "关机",
|
||||
"ShutdownForce": "强制关机",
|
||||
"Hibernate": "休眠",
|
||||
"Sleep": "睡眠",
|
||||
"KillSelf": "退出程序",
|
||||
}
|
||||
"""电源操作类型索引表"""
|
||||
|
||||
RESERVED_NAMES = {
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"NUL",
|
||||
"COM1",
|
||||
"COM2",
|
||||
"COM3",
|
||||
"COM4",
|
||||
"COM5",
|
||||
"COM6",
|
||||
"COM7",
|
||||
"COM8",
|
||||
"COM9",
|
||||
"LPT1",
|
||||
"LPT2",
|
||||
"LPT3",
|
||||
"LPT4",
|
||||
"LPT5",
|
||||
"LPT6",
|
||||
"LPT7",
|
||||
"LPT8",
|
||||
"LPT9",
|
||||
}
|
||||
"""Windows保留名称列表"""
|
||||
|
||||
ILLEGAL_CHARS = set('<>:"/\\|?*')
|
||||
"""文件名非法字符集合"""
|
||||
|
||||
MIRROR_ERROR_INFO = {
|
||||
1001: "获取版本信息的URL参数不正确",
|
||||
7001: "填入的 CDK 已过期",
|
||||
7002: "填入的 CDK 错误",
|
||||
7003: "填入的 CDK 今日下载次数已达上限",
|
||||
7004: "填入的 CDK 类型和待下载的资源不匹配",
|
||||
7005: "填入的 CDK 已被封禁",
|
||||
8001: "对应架构和系统下的资源不存在",
|
||||
8002: "错误的系统参数",
|
||||
8003: "错误的架构参数",
|
||||
8004: "错误的更新通道参数",
|
||||
1: "未知错误类型",
|
||||
}
|
||||
"""MirrorChyan错误代码映射表"""
|
||||
|
||||
DEFAULT_DATETIME = datetime.strptime("2000-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
|
||||
"""默认日期时间"""
|
||||
5
app/utils/device_manager/__init__.py
Normal file
5
app/utils/device_manager/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .mumu import MumuManager
|
||||
from .ldplayer import LDManager
|
||||
from .utils import BaseDevice, DeviceStatus
|
||||
|
||||
__all__ = ["MumuManager", "LDManager", "BaseDevice", "DeviceStatus"]
|
||||
492
app/utils/device_manager/general.py
Normal file
492
app/utils/device_manager/general.py
Normal file
@@ -0,0 +1,492 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import asyncio
|
||||
import psutil
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from app.utils.device_manager.utils import BaseDevice, DeviceStatus
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""进程监视器类, 用于跟踪主进程及其所有子进程的状态"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.main_pid = None
|
||||
self.tracked_pids = set()
|
||||
self.check_task = None
|
||||
self.track_end_time = datetime.now()
|
||||
|
||||
async def open_process(
|
||||
self, path: Path, args: list = [], tracking_time: int = 60
|
||||
) -> None:
|
||||
"""
|
||||
启动一个新进程并返回其pid, 并开始监视该进程
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path: 可执行文件的路径
|
||||
args: 启动参数列表
|
||||
tracking_time: 子进程追踪持续时间(秒)
|
||||
"""
|
||||
|
||||
process = subprocess.Popen(
|
||||
[path, *args],
|
||||
cwd=path.parent,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
await self.start_monitoring(process.pid, tracking_time)
|
||||
|
||||
async def start_monitoring(self, pid: int, tracking_time: int = 60) -> None:
|
||||
"""
|
||||
启动进程监视器, 跟踪指定的主进程及其子进程
|
||||
|
||||
:param pid: 被监视进程的PID
|
||||
:param tracking_time: 子进程追踪持续时间(秒)
|
||||
"""
|
||||
|
||||
await self.clear()
|
||||
|
||||
self.main_pid = pid
|
||||
self.tracking_time = tracking_time
|
||||
|
||||
# 扫描并记录所有相关进程
|
||||
try:
|
||||
# 获取主进程
|
||||
main_proc = psutil.Process(self.main_pid)
|
||||
self.tracked_pids.add(self.main_pid)
|
||||
|
||||
# 递归获取所有子进程
|
||||
if tracking_time:
|
||||
for child in main_proc.children(recursive=True):
|
||||
self.tracked_pids.add(child.pid)
|
||||
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
# 启动持续追踪任务
|
||||
if tracking_time > 0:
|
||||
self.track_end_time = datetime.now() + timedelta(seconds=tracking_time)
|
||||
self.check_task = asyncio.create_task(self.track_processes())
|
||||
|
||||
async def track_processes(self) -> None:
|
||||
"""更新子进程列表"""
|
||||
|
||||
while datetime.now() < self.track_end_time:
|
||||
current_pids = set(self.tracked_pids)
|
||||
for pid in current_pids:
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
for child in proc.children():
|
||||
if child.pid not in self.tracked_pids:
|
||||
# 新发现的子进程
|
||||
self.tracked_pids.add(child.pid)
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def is_running(self) -> bool:
|
||||
"""检查所有跟踪的进程是否还在运行"""
|
||||
|
||||
for pid in self.tracked_pids:
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
if proc.is_running():
|
||||
return True
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
async def kill(self, if_force: bool = False) -> None:
|
||||
"""停止监视器并中止所有跟踪的进程"""
|
||||
|
||||
for pid in self.tracked_pids:
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
if if_force:
|
||||
kill_process = subprocess.Popen(
|
||||
["taskkill", "/F", "/T", "/PID", str(pid)],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
kill_process.wait()
|
||||
proc.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
|
||||
await self.clear()
|
||||
|
||||
async def clear(self) -> None:
|
||||
"""清空跟踪的进程列表"""
|
||||
|
||||
if self.check_task is not None and not self.check_task.done():
|
||||
self.check_task.cancel()
|
||||
|
||||
try:
|
||||
await self.check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
self.main_pid = None
|
||||
self.tracked_pids.clear()
|
||||
|
||||
|
||||
class GeneralDeviceManager(BaseDevice):
|
||||
"""
|
||||
通用设备管理器,基于BaseDevice和ProcessManager实现
|
||||
用于管理一般应用程序进程
|
||||
"""
|
||||
|
||||
def __init__(self, executable_path: str, name: str = "通用设备"):
|
||||
"""
|
||||
初始化通用设备管理器
|
||||
|
||||
Args:
|
||||
executable_path (str): 可执行文件的绝对路径
|
||||
name (str): 设备管理器名称
|
||||
"""
|
||||
self.executable_path = Path(executable_path)
|
||||
self.name = name
|
||||
self.logger = get_logger(f"{name}管理器")
|
||||
|
||||
# 进程管理实例字典,以idx为键
|
||||
self.process_managers: Dict[str, ProcessManager] = {}
|
||||
|
||||
# 设备信息存储
|
||||
self.device_info: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# 默认等待时间
|
||||
self.wait_time = 60
|
||||
|
||||
if not self.executable_path.exists():
|
||||
raise FileNotFoundError(f"可执行文件不存在: {executable_path}")
|
||||
|
||||
async def start(self, idx: str, package_name: str = "") -> tuple[bool, int, dict]:
|
||||
"""
|
||||
启动设备
|
||||
|
||||
Args:
|
||||
idx: 设备ID
|
||||
package_name: 包名(可选)
|
||||
|
||||
Returns:
|
||||
tuple[bool, int, dict]: (是否成功, 状态码, 启动信息)
|
||||
"""
|
||||
try:
|
||||
# 检查是否已经在运行
|
||||
current_status = await self.get_status(idx)
|
||||
if current_status in [DeviceStatus.ONLINE, DeviceStatus.STARTING]:
|
||||
self.logger.warning(f"设备{idx}已经在运行,状态: {current_status}")
|
||||
return False, current_status, {}
|
||||
|
||||
# 创建进程管理器
|
||||
if idx not in self.process_managers:
|
||||
self.process_managers[idx] = ProcessManager()
|
||||
|
||||
# 准备启动参数
|
||||
args = []
|
||||
if package_name:
|
||||
args.extend(["-pkg", package_name])
|
||||
|
||||
# 启动进程
|
||||
await self.process_managers[idx].open_process(
|
||||
self.executable_path, args, tracking_time=self.wait_time
|
||||
)
|
||||
|
||||
# 等待进程启动
|
||||
start_time = datetime.now()
|
||||
timeout = timedelta(seconds=self.wait_time)
|
||||
|
||||
while datetime.now() - start_time < timeout:
|
||||
if await self.process_managers[idx].is_running():
|
||||
self.device_info[idx] = {
|
||||
"title": f"{self.name}_{idx}",
|
||||
"status": str(DeviceStatus.ONLINE),
|
||||
"pid": self.process_managers[idx].main_pid,
|
||||
"start_time": start_time.isoformat(),
|
||||
}
|
||||
|
||||
self.logger.info(f"设备{idx}启动成功")
|
||||
return True, DeviceStatus.ONLINE, self.device_info[idx]
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
self.logger.error(f"设备{idx}启动超时")
|
||||
return False, DeviceStatus.ERROR, {}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"启动设备{idx}失败: {str(e)}")
|
||||
return False, DeviceStatus.ERROR, {}
|
||||
|
||||
async def close(self, idx: str) -> tuple[bool, int]:
|
||||
"""
|
||||
关闭设备或服务
|
||||
|
||||
Args:
|
||||
idx: 设备ID
|
||||
|
||||
Returns:
|
||||
tuple[bool, int]: (是否成功, 状态码)
|
||||
"""
|
||||
try:
|
||||
if idx not in self.process_managers:
|
||||
self.logger.warning(f"设备{idx}的进程管理器不存在")
|
||||
return False, DeviceStatus.NOT_FOUND
|
||||
|
||||
# 检查进程是否在运行
|
||||
if not await self.process_managers[idx].is_running():
|
||||
self.logger.info(f"设备{idx}进程已经停止")
|
||||
return True, DeviceStatus.OFFLINE
|
||||
|
||||
# 终止进程
|
||||
await self.process_managers[idx].kill(if_force=False)
|
||||
|
||||
# 等待进程完全停止
|
||||
stop_time = datetime.now()
|
||||
timeout = timedelta(seconds=10) # 10秒超时
|
||||
|
||||
while datetime.now() - stop_time < timeout:
|
||||
if not await self.process_managers[idx].is_running():
|
||||
# 清理设备信息
|
||||
if idx in self.device_info:
|
||||
del self.device_info[idx]
|
||||
|
||||
self.logger.info(f"设备{idx}已成功关闭")
|
||||
return True, DeviceStatus.OFFLINE
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 强制终止
|
||||
self.logger.warning(f"设备{idx}未能正常关闭,尝试强制终止")
|
||||
await self.process_managers[idx].kill(if_force=True)
|
||||
|
||||
if idx in self.device_info:
|
||||
del self.device_info[idx]
|
||||
|
||||
return True, DeviceStatus.OFFLINE
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"关闭设备{idx}失败: {str(e)}")
|
||||
return False, DeviceStatus.ERROR
|
||||
|
||||
async def get_status(self, idx: str) -> int:
|
||||
"""
|
||||
获取指定设备当前状态
|
||||
|
||||
Args:
|
||||
idx: 设备ID
|
||||
|
||||
Returns:
|
||||
int: 状态码
|
||||
"""
|
||||
try:
|
||||
if idx not in self.process_managers:
|
||||
return DeviceStatus.OFFLINE
|
||||
|
||||
if await self.process_managers[idx].is_running():
|
||||
return DeviceStatus.ONLINE
|
||||
else:
|
||||
return DeviceStatus.OFFLINE
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取设备{idx}状态失败: {str(e)}")
|
||||
return DeviceStatus.ERROR
|
||||
|
||||
async def hide_device(self, idx: str) -> tuple[bool, int]:
|
||||
"""
|
||||
隐藏设备窗口
|
||||
|
||||
Args:
|
||||
idx: 设备ID
|
||||
|
||||
Returns:
|
||||
tuple[bool, int]: (是否成功, 状态码)
|
||||
"""
|
||||
try:
|
||||
status = await self.get_status(idx)
|
||||
if status != DeviceStatus.ONLINE:
|
||||
return False, status
|
||||
|
||||
if (
|
||||
idx not in self.process_managers
|
||||
or not self.process_managers[idx].main_pid
|
||||
):
|
||||
return False, DeviceStatus.NOT_FOUND
|
||||
|
||||
# 窗口隐藏功能(简化实现)
|
||||
# 注意:完整的窗口隐藏功能需要更复杂的Windows API调用
|
||||
self.logger.info(f"设备{idx}窗口隐藏请求已处理(简化实现)")
|
||||
return True, DeviceStatus.ONLINE
|
||||
|
||||
self.logger.info(f"设备{idx}窗口已隐藏")
|
||||
return True, DeviceStatus.ONLINE
|
||||
|
||||
except ImportError:
|
||||
self.logger.warning("隐藏窗口功能需要pywin32库")
|
||||
return False, DeviceStatus.ERROR
|
||||
except Exception as e:
|
||||
self.logger.error(f"隐藏设备{idx}窗口失败: {str(e)}")
|
||||
return False, DeviceStatus.ERROR
|
||||
|
||||
async def show_device(self, idx: str) -> tuple[bool, int]:
|
||||
"""
|
||||
显示设备窗口
|
||||
|
||||
Args:
|
||||
idx: 设备ID
|
||||
|
||||
Returns:
|
||||
tuple[bool, int]: (是否成功, 状态码)
|
||||
"""
|
||||
try:
|
||||
status = await self.get_status(idx)
|
||||
if status != DeviceStatus.ONLINE:
|
||||
return False, status
|
||||
|
||||
if (
|
||||
idx not in self.process_managers
|
||||
or not self.process_managers[idx].main_pid
|
||||
):
|
||||
return False, DeviceStatus.NOT_FOUND
|
||||
|
||||
# 窗口显示功能(简化实现)
|
||||
# 注意:完整的窗口显示功能需要更复杂的Windows API调用
|
||||
self.logger.info(f"设备{idx}窗口显示请求已处理(简化实现)")
|
||||
return True, DeviceStatus.ONLINE
|
||||
|
||||
self.logger.info(f"设备{idx}窗口已显示")
|
||||
return True, DeviceStatus.ONLINE
|
||||
|
||||
except ImportError:
|
||||
self.logger.warning("显示窗口功能需要pywin32库")
|
||||
return False, DeviceStatus.ERROR
|
||||
except Exception as e:
|
||||
self.logger.error(f"显示设备{idx}窗口失败: {str(e)}")
|
||||
return False, DeviceStatus.ERROR
|
||||
|
||||
async def get_all_info(self) -> dict[str, dict[str, str]]:
|
||||
"""
|
||||
获取所有设备信息
|
||||
|
||||
Returns:
|
||||
dict[str, dict[str, str]]: 设备信息字典
|
||||
结构示例:
|
||||
{
|
||||
"0": {
|
||||
"title": "设备名称",
|
||||
"status": "1"
|
||||
}
|
||||
}
|
||||
"""
|
||||
result = {}
|
||||
|
||||
for idx in list(self.process_managers.keys()):
|
||||
try:
|
||||
status = await self.get_status(idx)
|
||||
|
||||
if idx in self.device_info:
|
||||
title = self.device_info[idx].get("title", f"{self.name}_{idx}")
|
||||
else:
|
||||
title = f"{self.name}_{idx}"
|
||||
|
||||
result[idx] = {"title": title, "status": str(status)}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取设备{idx}信息失败: {str(e)}")
|
||||
result[idx] = {
|
||||
"title": f"{self.name}_{idx}",
|
||||
"status": str(DeviceStatus.ERROR),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
"""
|
||||
清理所有资源
|
||||
"""
|
||||
self.logger.info("开始清理设备管理器资源")
|
||||
|
||||
for idx, pm in list(self.process_managers.items()):
|
||||
try:
|
||||
if await pm.is_running():
|
||||
await pm.kill(if_force=True)
|
||||
await pm.clear()
|
||||
except Exception as e:
|
||||
self.logger.error(f"清理设备{idx}资源失败: {str(e)}")
|
||||
|
||||
self.process_managers.clear()
|
||||
self.device_info.clear()
|
||||
|
||||
self.logger.info("设备管理器资源清理完成")
|
||||
|
||||
def __del__(self):
|
||||
"""析构函数,确保资源被正确释放"""
|
||||
try:
|
||||
# 注意:析构函数中不能使用async/await
|
||||
# 这里只是标记,实际清理需要显式调用cleanup()
|
||||
if hasattr(self, "process_managers") and self.process_managers:
|
||||
self.logger.warning("设备管理器未正确清理,请显式调用cleanup()方法")
|
||||
except: # noqa: E722
|
||||
pass
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
|
||||
async def main():
|
||||
# 创建通用设备管理器
|
||||
manager = GeneralDeviceManager(
|
||||
executable_path=r"C:\Windows\System32\notepad.exe", name="记事本"
|
||||
)
|
||||
|
||||
try:
|
||||
# 启动设备
|
||||
success, status, info = await manager.start("0")
|
||||
print(f"启动结果: {success}, 状态: {status}, 信息: {info}")
|
||||
|
||||
if success:
|
||||
# 获取所有设备信息
|
||||
all_info = await manager.get_all_info()
|
||||
print(f"所有设备信息: {all_info}")
|
||||
|
||||
# 等待5秒
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# 关闭设备
|
||||
close_success, close_status = await manager.close("0")
|
||||
print(f"关闭结果: {close_success}, 状态: {close_status}")
|
||||
|
||||
finally:
|
||||
# 清理资源
|
||||
await manager.cleanup()
|
||||
|
||||
# 运行示例
|
||||
asyncio.run(main())
|
||||
278
app/utils/device_manager/keyboard_utils.py
Normal file
278
app/utils/device_manager/keyboard_utils.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
键盘工具模块
|
||||
|
||||
提供虚拟键码到keyboard库按键名称的转换功能,以及相关的键盘操作辅助函数。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import keyboard
|
||||
from typing import List
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("键盘工具")
|
||||
|
||||
|
||||
def vk_code_to_key_name(vk_code: int) -> str:
|
||||
"""
|
||||
将Windows虚拟键码转换为keyboard库识别的按键名称
|
||||
|
||||
Args:
|
||||
vk_code (int): Windows虚拟键码
|
||||
|
||||
Returns:
|
||||
str: keyboard库识别的按键名称
|
||||
|
||||
Examples:
|
||||
>>> vk_code_to_key_name(0x1B)
|
||||
'esc'
|
||||
>>> vk_code_to_key_name(0x70)
|
||||
'f1'
|
||||
>>> vk_code_to_key_name(0x41)
|
||||
'a'
|
||||
"""
|
||||
# Windows虚拟键码到keyboard库按键名称的映射
|
||||
vk_mapping = {
|
||||
# 常用功能键
|
||||
0x1B: "esc", # VK_ESCAPE
|
||||
0x0D: "enter", # VK_RETURN
|
||||
0x20: "space", # VK_SPACE
|
||||
0x08: "backspace", # VK_BACK
|
||||
0x09: "tab", # VK_TAB
|
||||
0x2E: "delete", # VK_DELETE
|
||||
0x24: "home", # VK_HOME
|
||||
0x23: "end", # VK_END
|
||||
0x21: "page up", # VK_PRIOR
|
||||
0x22: "page down", # VK_NEXT
|
||||
0x2D: "insert", # VK_INSERT
|
||||
# 修饰键
|
||||
0x10: "shift", # VK_SHIFT
|
||||
0x11: "ctrl", # VK_CONTROL
|
||||
0x12: "alt", # VK_MENU
|
||||
0x5B: "left windows", # VK_LWIN
|
||||
0x5C: "right windows", # VK_RWIN
|
||||
0x5D: "apps", # VK_APPS (右键菜单键)
|
||||
# 方向键
|
||||
0x25: "left", # VK_LEFT
|
||||
0x26: "up", # VK_UP
|
||||
0x27: "right", # VK_RIGHT
|
||||
0x28: "down", # VK_DOWN
|
||||
# 功能键 F1-F12
|
||||
0x70: "f1",
|
||||
0x71: "f2",
|
||||
0x72: "f3",
|
||||
0x73: "f4",
|
||||
0x74: "f5",
|
||||
0x75: "f6",
|
||||
0x76: "f7",
|
||||
0x77: "f8",
|
||||
0x78: "f9",
|
||||
0x79: "f10",
|
||||
0x7A: "f11",
|
||||
0x7B: "f12",
|
||||
# 数字键 0-9
|
||||
0x30: "0",
|
||||
0x31: "1",
|
||||
0x32: "2",
|
||||
0x33: "3",
|
||||
0x34: "4",
|
||||
0x35: "5",
|
||||
0x36: "6",
|
||||
0x37: "7",
|
||||
0x38: "8",
|
||||
0x39: "9",
|
||||
# 字母键 A-Z
|
||||
0x41: "a",
|
||||
0x42: "b",
|
||||
0x43: "c",
|
||||
0x44: "d",
|
||||
0x45: "e",
|
||||
0x46: "f",
|
||||
0x47: "g",
|
||||
0x48: "h",
|
||||
0x49: "i",
|
||||
0x4A: "j",
|
||||
0x4B: "k",
|
||||
0x4C: "l",
|
||||
0x4D: "m",
|
||||
0x4E: "n",
|
||||
0x4F: "o",
|
||||
0x50: "p",
|
||||
0x51: "q",
|
||||
0x52: "r",
|
||||
0x53: "s",
|
||||
0x54: "t",
|
||||
0x55: "u",
|
||||
0x56: "v",
|
||||
0x57: "w",
|
||||
0x58: "x",
|
||||
0x59: "y",
|
||||
0x5A: "z",
|
||||
# 数字小键盘
|
||||
0x60: "num 0",
|
||||
0x61: "num 1",
|
||||
0x62: "num 2",
|
||||
0x63: "num 3",
|
||||
0x64: "num 4",
|
||||
0x65: "num 5",
|
||||
0x66: "num 6",
|
||||
0x67: "num 7",
|
||||
0x68: "num 8",
|
||||
0x69: "num 9",
|
||||
0x6A: "num *",
|
||||
0x6B: "num +",
|
||||
0x6D: "num -",
|
||||
0x6E: "num .",
|
||||
0x6F: "num /",
|
||||
0x90: "num lock",
|
||||
# 标点符号和特殊键
|
||||
0xBA: ";", # VK_OEM_1 (;:)
|
||||
0xBB: "=", # VK_OEM_PLUS (=+)
|
||||
0xBC: ",", # VK_OEM_COMMA (,<)
|
||||
0xBD: "-", # VK_OEM_MINUS (-_)
|
||||
0xBE: ".", # VK_OEM_PERIOD (.>)
|
||||
0xBF: "/", # VK_OEM_2 (/?)
|
||||
0xC0: "`", # VK_OEM_3 (`~)
|
||||
0xDB: "[", # VK_OEM_4 ([{)
|
||||
0xDC: "\\", # VK_OEM_5 (\|)
|
||||
0xDD: "]", # VK_OEM_6 (]})
|
||||
0xDE: "'", # VK_OEM_7 ('")
|
||||
# 系统键
|
||||
0x14: "caps lock", # VK_CAPITAL
|
||||
0x91: "scroll lock", # VK_SCROLL
|
||||
0x13: "pause", # VK_PAUSE
|
||||
0x2C: "print screen", # VK_SNAPSHOT
|
||||
# 媒体键
|
||||
0xA0: "left shift", # VK_LSHIFT
|
||||
0xA1: "right shift", # VK_RSHIFT
|
||||
0xA2: "left ctrl", # VK_LCONTROL
|
||||
0xA3: "right ctrl", # VK_RCONTROL
|
||||
0xA4: "left alt", # VK_LMENU
|
||||
0xA5: "right alt", # VK_RMENU
|
||||
}
|
||||
|
||||
return vk_mapping.get(vk_code, f"vk_{vk_code}")
|
||||
|
||||
|
||||
def vk_codes_to_key_names(vk_codes: List[int]) -> List[str]:
|
||||
"""
|
||||
将多个虚拟键码转换为keyboard库识别的按键名称列表
|
||||
|
||||
Args:
|
||||
vk_codes (List[int]): Windows虚拟键码列表
|
||||
|
||||
Returns:
|
||||
List[str]: keyboard库识别的按键名称列表
|
||||
|
||||
Examples:
|
||||
>>> vk_codes_to_key_names([0x11, 0x12, 0x44])
|
||||
['ctrl', 'alt', 'd']
|
||||
"""
|
||||
return [vk_code_to_key_name(vk) for vk in vk_codes]
|
||||
|
||||
|
||||
async def send_key_combination(key_names: List[str], hold_time: float = 0.05) -> bool:
|
||||
"""
|
||||
发送按键组合
|
||||
|
||||
Args:
|
||||
key_names (List[str]): 按键名称列表
|
||||
hold_time (float): 按键保持时间(秒),默认 0.05 秒
|
||||
|
||||
Returns:
|
||||
bool: 操作是否成功
|
||||
|
||||
Examples:
|
||||
>>> await send_key_combination(['ctrl', 'alt', 'd'])
|
||||
True
|
||||
>>> await send_key_combination(['f1'])
|
||||
True
|
||||
"""
|
||||
try:
|
||||
if not key_names:
|
||||
logger.warning("按键名称列表为空")
|
||||
return False
|
||||
|
||||
if len(key_names) == 1:
|
||||
# 单个按键
|
||||
keyboard.press_and_release(key_names[0])
|
||||
logger.debug(f"发送单个按键: {key_names[0]}")
|
||||
else:
|
||||
# 组合键:按下所有键,然后释放
|
||||
logger.debug(f"发送组合键: {'+'.join(key_names)}")
|
||||
for key in key_names:
|
||||
keyboard.press(key)
|
||||
await asyncio.sleep(hold_time) # 保持按键状态
|
||||
for key in reversed(key_names):
|
||||
keyboard.release(key)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送按键组合失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def send_vk_combination(vk_codes: List[int], hold_time: float = 0.05) -> bool:
|
||||
"""
|
||||
发送虚拟键码组合
|
||||
|
||||
Args:
|
||||
vk_codes (List[int]): Windows虚拟键码列表
|
||||
hold_time (float): 按键保持时间(秒),默认 0.05 秒
|
||||
|
||||
Returns:
|
||||
bool: 操作是否成功
|
||||
|
||||
Examples:
|
||||
>>> await send_vk_combination([0x11, 0x12, 0x44]) # Ctrl+Alt+D
|
||||
True
|
||||
>>> await send_vk_combination([0x70]) # F1
|
||||
True
|
||||
"""
|
||||
try:
|
||||
key_names = vk_codes_to_key_names(vk_codes)
|
||||
return await send_key_combination(key_names, hold_time)
|
||||
except Exception as e:
|
||||
logger.error(f"发送虚拟键码组合失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_common_boss_keys() -> dict[str, List[int]]:
|
||||
"""
|
||||
获取常见的BOSS键组合
|
||||
|
||||
Returns:
|
||||
dict[str, List[int]]: 常见BOSS键组合的名称和对应的虚拟键码
|
||||
"""
|
||||
return {
|
||||
"alt_tab": [0x12, 0x09], # Alt+Tab
|
||||
"ctrl_alt_d": [0x11, 0x12, 0x44], # Ctrl+Alt+D
|
||||
"win_d": [0x5B, 0x44], # Win+D (显示桌面)
|
||||
"win_m": [0x5B, 0x4D], # Win+M (最小化所有窗口)
|
||||
"f1": [0x70], # F1
|
||||
"f2": [0x71], # F2
|
||||
"f3": [0x72], # F3
|
||||
"f4": [0x73], # F4
|
||||
"alt_f4": [0x12, 0x73], # Alt+F4 (关闭窗口)
|
||||
"ctrl_shift_esc": [0x11, 0x10, 0x1B], # Ctrl+Shift+Esc (任务管理器)
|
||||
}
|
||||
|
||||
|
||||
def describe_vk_combination(vk_codes: List[int]) -> str:
|
||||
"""
|
||||
描述虚拟键码组合
|
||||
|
||||
Args:
|
||||
vk_codes (List[int]): Windows虚拟键码列表
|
||||
|
||||
Returns:
|
||||
str: 组合键的描述字符串
|
||||
|
||||
Examples:
|
||||
>>> describe_vk_combination([0x11, 0x12, 0x44])
|
||||
'Ctrl+Alt+D'
|
||||
>>> describe_vk_combination([0x70])
|
||||
'F1'
|
||||
"""
|
||||
key_names = vk_codes_to_key_names(vk_codes)
|
||||
return "+".join(name.title() for name in key_names)
|
||||
329
app/utils/device_manager/ldplayer.py
Normal file
329
app/utils/device_manager/ldplayer.py
Normal file
@@ -0,0 +1,329 @@
|
||||
import asyncio
|
||||
from typing import Literal
|
||||
from app.utils.device_manager.utils import BaseDevice, ExeRunner, DeviceStatus
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.device_manager.keyboard_utils import (
|
||||
vk_codes_to_key_names,
|
||||
send_key_combination,
|
||||
)
|
||||
import psutil
|
||||
from pydantic import BaseModel
|
||||
import win32gui
|
||||
|
||||
|
||||
class EmulatorInfo(BaseModel):
|
||||
idx: int
|
||||
title: str
|
||||
top_hwnd: int
|
||||
bind_hwnd: int
|
||||
in_android: int
|
||||
pid: int
|
||||
vbox_pid: int
|
||||
width: int
|
||||
height: int
|
||||
density: int
|
||||
|
||||
|
||||
class LDManager(BaseDevice):
|
||||
"""
|
||||
基于dnconsole.exe的模拟器管理
|
||||
|
||||
!需要管理员权限
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, exe_path: str) -> None:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
exe_path (str): dnconsole.exe的绝对路径
|
||||
"""
|
||||
self.runner = ExeRunner(exe_path, "gbk")
|
||||
self.logger = get_logger("雷电模拟器管理器")
|
||||
self.wait_time = 60 # 配置获取 后续改一下 单位为s
|
||||
|
||||
async def start(self, idx: str, package_name="") -> tuple[bool, int, dict]:
|
||||
"""
|
||||
启动指定模拟器
|
||||
Returns:
|
||||
tuple[bool, int, str]: 是否成功, 当前状态码, ADB端口信息
|
||||
"""
|
||||
OK, info, status = await self.get_device_info(idx)
|
||||
if status != DeviceStatus.OFFLINE:
|
||||
self.logger.error(
|
||||
f"模拟器{idx}未处于关闭状态,当前状态码: {status}, 需求状态码: {DeviceStatus.OFFLINE}"
|
||||
)
|
||||
return False, status, {}
|
||||
if package_name:
|
||||
result = self.runner.run(
|
||||
"launch",
|
||||
"--index",
|
||||
idx,
|
||||
"--packagename",
|
||||
f'"{package_name}"',
|
||||
)
|
||||
else:
|
||||
result = self.runner.run(
|
||||
"launch",
|
||||
"--index",
|
||||
idx,
|
||||
)
|
||||
# 参考命令 dnconsole.exe launch --index 0
|
||||
self.logger.debug(f"启动结果:{result}")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"命令执行失败: {result}")
|
||||
|
||||
i = 0
|
||||
while i < self.wait_time * 10:
|
||||
OK, info, status = await self.get_device_info(idx)
|
||||
if status == DeviceStatus.ERROR or status == DeviceStatus.UNKNOWN:
|
||||
self.logger.error(f"模拟器{idx}启动失败,状态码: {status}")
|
||||
return False, status, {}
|
||||
if status == DeviceStatus.ONLINE:
|
||||
self.logger.debug(info)
|
||||
if OK and isinstance(info, EmulatorInfo):
|
||||
pid: int = info.vbox_pid
|
||||
adb_port = ""
|
||||
adb_host_ip = await self.get_adb_ports(pid)
|
||||
print(adb_host_ip)
|
||||
if adb_host_ip:
|
||||
return (
|
||||
True,
|
||||
status,
|
||||
{"adb_port": adb_port, "adb_host_ip": adb_host_ip},
|
||||
)
|
||||
|
||||
return True, status, {}
|
||||
await asyncio.sleep(0.1)
|
||||
i += 1
|
||||
return False, DeviceStatus.UNKNOWN, {}
|
||||
|
||||
async def close(self, idx: str) -> tuple[bool, int]:
|
||||
"""
|
||||
关闭指定模拟器
|
||||
Returns:
|
||||
- tuple[bool, int]: 是否成功, 当前状态码
|
||||
|
||||
参考命令行:dnconsole.exe quit --index 0
|
||||
"""
|
||||
OK, info, status = await self.get_device_info(idx)
|
||||
if status != DeviceStatus.ONLINE and status != DeviceStatus.STARTING:
|
||||
return False, DeviceStatus.NOT_FOUND
|
||||
result = self.runner.run(
|
||||
"quit",
|
||||
"--index",
|
||||
idx,
|
||||
)
|
||||
# 参考命令 dnconsole.exe quit --index 0
|
||||
if result.returncode != 0:
|
||||
return True, DeviceStatus.OFFLINE
|
||||
i = 0
|
||||
while i < self.wait_time * 10:
|
||||
OK, info, status = await self.get_device_info(idx)
|
||||
if status == DeviceStatus.ERROR or status == DeviceStatus.UNKNOWN:
|
||||
return False, status
|
||||
if status == DeviceStatus.OFFLINE:
|
||||
return True, DeviceStatus.OFFLINE
|
||||
await asyncio.sleep(0.1)
|
||||
i += 1
|
||||
|
||||
return False, DeviceStatus.UNKNOWN
|
||||
|
||||
async def get_status(self, idx: str) -> int:
|
||||
"""
|
||||
获取指定模拟器当前状态
|
||||
返回值: 状态码
|
||||
"""
|
||||
_, _, status = await self.get_device_info(idx)
|
||||
return status
|
||||
|
||||
async def get_device_info(
|
||||
self,
|
||||
idx: str,
|
||||
data: dict[int, EmulatorInfo] | None = None,
|
||||
) -> tuple[Literal[True], EmulatorInfo, int] | tuple[Literal[False], dict, int]:
|
||||
"""
|
||||
获取指定模拟器的信息和状态
|
||||
Returns:
|
||||
- tuple[bool, EmulatorInfo | dict, int]: 是否成功, 模拟器信息或空字典, 状态码
|
||||
|
||||
参考命令行:dnconsole.exe list2
|
||||
"""
|
||||
if not data:
|
||||
result = await self._get_all_info()
|
||||
else:
|
||||
result = data
|
||||
|
||||
try:
|
||||
emulator_info = result.get(int(idx))
|
||||
print(emulator_info)
|
||||
if not emulator_info:
|
||||
self.logger.error(f"未找到模拟器{idx}的信息")
|
||||
return False, {}, DeviceStatus.UNKNOWN
|
||||
|
||||
self.logger.debug(f"获取模拟器{idx}信息: {emulator_info}")
|
||||
|
||||
# 计算状态码
|
||||
if emulator_info.in_android == 1:
|
||||
status = DeviceStatus.ONLINE
|
||||
elif emulator_info.in_android == 2:
|
||||
if emulator_info.vbox_pid > 0:
|
||||
status = DeviceStatus.STARTING
|
||||
# 雷电启动后, vbox_pid为-1, 目前不知道有什么区别
|
||||
else:
|
||||
status = DeviceStatus.STARTING
|
||||
elif emulator_info.in_android == 0:
|
||||
status = DeviceStatus.OFFLINE
|
||||
else:
|
||||
status = DeviceStatus.UNKNOWN
|
||||
|
||||
self.logger.debug(f"获取模拟器{idx}状态: {status}")
|
||||
return True, emulator_info, status
|
||||
except: # noqa: E722
|
||||
self.logger.error(f"获取模拟器{idx}信息失败")
|
||||
return False, {}, DeviceStatus.UNKNOWN
|
||||
|
||||
async def _get_all_info(self) -> dict[int, EmulatorInfo]:
|
||||
result = self.runner.run("list2")
|
||||
# self.logger.debug(f"全部信息{result.stdout.strip()}")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"命令执行失败: {result}")
|
||||
|
||||
emulators: dict[int, EmulatorInfo] = {}
|
||||
data = result.stdout.strip()
|
||||
|
||||
for line in data.strip().splitlines():
|
||||
parts = line.strip().split(",")
|
||||
if len(parts) != 10:
|
||||
raise ValueError(f"数据格式错误: {line}")
|
||||
try:
|
||||
info = EmulatorInfo(
|
||||
idx=int(parts[0]),
|
||||
title=parts[1],
|
||||
top_hwnd=int(parts[2]),
|
||||
bind_hwnd=int(parts[3]),
|
||||
in_android=int(parts[4]),
|
||||
pid=int(parts[5]),
|
||||
vbox_pid=int(parts[6]),
|
||||
width=int(parts[7]),
|
||||
height=int(parts[8]),
|
||||
density=int(parts[9]),
|
||||
)
|
||||
emulators[info.idx] = info
|
||||
except Exception as e:
|
||||
self.logger.warning(f"解析失败: {line}, 错误: {e}")
|
||||
pass
|
||||
return emulators
|
||||
|
||||
# ?wk雷电你都返回了什么啊
|
||||
|
||||
async def get_all_info(self) -> dict[str, dict[str, str]]:
|
||||
"""
|
||||
解析_emulator_info字典,提取idx和title,便于前端显示
|
||||
"""
|
||||
raw_data = await self._get_all_info()
|
||||
result: dict[str, dict[str, str]] = {}
|
||||
for info in raw_data.values():
|
||||
OK, device_info, status = await self.get_device_info(
|
||||
str(info.idx), raw_data
|
||||
)
|
||||
result[str(info.idx)] = {"title": info.title, "status": str(status)}
|
||||
return result
|
||||
|
||||
async def send_boss_key(
|
||||
self,
|
||||
boss_keys: list[int],
|
||||
result: EmulatorInfo,
|
||||
is_show: bool = False,
|
||||
# True: 显示, False: 隐藏
|
||||
) -> bool:
|
||||
"""
|
||||
发送BOSS键
|
||||
|
||||
Args:
|
||||
idx (str): 模拟器索引
|
||||
boss_keys (list[int]): BOSS键的虚拟键码列表
|
||||
result (EmulatorInfo): 模拟器信息
|
||||
is_show (bool, optional): 将要隐藏或显示窗口,默认为 False(隐藏)。
|
||||
"""
|
||||
hwnd = result.top_hwnd
|
||||
try:
|
||||
# 使用键盘工具发送按键组合
|
||||
success = await send_key_combination(vk_codes_to_key_names(boss_keys))
|
||||
if not success:
|
||||
return False
|
||||
|
||||
# 等待系统处理
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 检查窗口可见性是否符合预期
|
||||
current_visible = win32gui.IsWindowVisible(hwnd)
|
||||
expected_visible = is_show
|
||||
|
||||
if current_visible == expected_visible:
|
||||
return True
|
||||
else:
|
||||
# 如果第一次没有成功,再试一次
|
||||
success = await send_key_combination(vk_codes_to_key_names(boss_keys))
|
||||
if not success:
|
||||
return False
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
current_visible = win32gui.IsWindowVisible(hwnd)
|
||||
return current_visible == expected_visible
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"发送BOSS键失败: {e}")
|
||||
return False
|
||||
|
||||
async def hide_device(
|
||||
self,
|
||||
idx: str,
|
||||
boss_keys: list[int] = [],
|
||||
) -> tuple[bool, int]:
|
||||
"""隐藏设备窗口"""
|
||||
OK, result, status = await self.get_device_info(idx)
|
||||
if not OK or not isinstance(result, EmulatorInfo):
|
||||
return False, DeviceStatus.UNKNOWN
|
||||
if status != DeviceStatus.ONLINE:
|
||||
return False, status
|
||||
|
||||
return await self.send_boss_key(boss_keys, result, False), status
|
||||
|
||||
async def show_device(
|
||||
self,
|
||||
idx: str,
|
||||
boss_keys: list[int] = [],
|
||||
) -> tuple[bool, int]:
|
||||
"""显示设备窗口"""
|
||||
OK, result, status = await self.get_device_info(idx)
|
||||
if not OK or not isinstance(result, EmulatorInfo):
|
||||
return False, DeviceStatus.UNKNOWN
|
||||
if status != DeviceStatus.ONLINE:
|
||||
return False, status
|
||||
|
||||
return await self.send_boss_key(boss_keys, result, True), status
|
||||
|
||||
async def get_adb_ports(self, pid: int) -> int:
|
||||
"""使用psutil获取adb端口"""
|
||||
try:
|
||||
process = psutil.Process(pid)
|
||||
connections = process.connections(kind="inet")
|
||||
for conn in connections:
|
||||
if conn.status == psutil.CONN_LISTEN and conn.laddr.port != 2222:
|
||||
return conn.laddr.port
|
||||
return 0 # 如果没有找到合适的端口,返回0
|
||||
except: # noqa: E722
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
MANAGER_PATH = (
|
||||
r"C:\leidian\LDPlayer9\dnconsole.exe" # 替换为实际的dnconsole.exe路径
|
||||
)
|
||||
idx = "0" # 替换为实际存在的模拟器实例索
|
||||
|
||||
manager = LDManager(MANAGER_PATH)
|
||||
# asyncio.run(manager._get_all_info())
|
||||
a = asyncio.run(manager.start("0"))
|
||||
print(a)
|
||||
220
app/utils/device_manager/mumu.py
Normal file
220
app/utils/device_manager/mumu.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import asyncio
|
||||
import json
|
||||
from app.utils.device_manager.utils import BaseDevice, ExeRunner, DeviceStatus
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
|
||||
class MumuManager(BaseDevice):
|
||||
"""
|
||||
基于MuMuManager.exe的模拟器管理
|
||||
"""
|
||||
|
||||
def __init__(self, exe_path: str) -> None:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
exe_path (str): MuMuManager.exe的绝对路径
|
||||
"""
|
||||
self.runner = ExeRunner(exe_path, "utf-8")
|
||||
self.logger = get_logger("MuMu管理器")
|
||||
self.wait_time = 60 # 配置获取 后续改一下 单位为s
|
||||
|
||||
async def start(self, idx: str, package_name="") -> tuple[bool, int, dict]:
|
||||
"""
|
||||
启动指定模拟器
|
||||
Returns:
|
||||
tuple[bool, int, str]: 是否成功, 当前状态码, ADB端口信息
|
||||
"""
|
||||
status = await self.get_status(idx)
|
||||
if status != DeviceStatus.OFFLINE:
|
||||
self.logger.error(
|
||||
f"模拟器{idx}未处于关闭状态,当前状态码: {status}, 需求状态码: {DeviceStatus.OFFLINE}"
|
||||
)
|
||||
return False, status, {}
|
||||
if package_name:
|
||||
result = self.runner.run(
|
||||
"control",
|
||||
"-v",
|
||||
idx,
|
||||
"launch",
|
||||
"-pkg",
|
||||
package_name,
|
||||
)
|
||||
else:
|
||||
result = self.runner.run(
|
||||
"control",
|
||||
"-v",
|
||||
idx,
|
||||
"launch",
|
||||
)
|
||||
# 参考命令 MuMuManager.exe control -v 2 launch
|
||||
self.logger.debug(f"启动结果:{result}")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"命令执行失败: {result}")
|
||||
|
||||
i = 0
|
||||
while i < self.wait_time * 10:
|
||||
status = await self.get_status(idx)
|
||||
if status == DeviceStatus.ERROR or status == DeviceStatus.UNKNOWN:
|
||||
self.logger.error(f"模拟器{idx}启动失败,状态码: {status}")
|
||||
return False, status, {}
|
||||
if status == DeviceStatus.ONLINE:
|
||||
OK, info = await self.get_device_info(idx)
|
||||
self.logger.debug(info)
|
||||
if OK:
|
||||
data = json.loads(info)
|
||||
adb_port = data.get("adb_port")
|
||||
adb_host_ip = data.get("adb_host_ip")
|
||||
if adb_port and adb_host_ip:
|
||||
return (
|
||||
True,
|
||||
status,
|
||||
{"adb_port": adb_port, "adb_host_ip": adb_host_ip},
|
||||
)
|
||||
|
||||
return True, status, {}
|
||||
await asyncio.sleep(0.1)
|
||||
i += 1
|
||||
return False, DeviceStatus.UNKNOWN, {}
|
||||
|
||||
async def close(self, idx: str) -> tuple[bool, int]:
|
||||
"""
|
||||
关闭指定模拟器
|
||||
Returns:
|
||||
tuple[bool, int]: 是否成功, 当前状态码
|
||||
"""
|
||||
status = await self.get_status(idx)
|
||||
if status != DeviceStatus.ONLINE and status != DeviceStatus.STARTING:
|
||||
return False, DeviceStatus.NOT_FOUND
|
||||
result = self.runner.run(
|
||||
"control",
|
||||
"-v",
|
||||
idx,
|
||||
"shutdown",
|
||||
)
|
||||
# 参考命令 MuMuManager.exe control -v 2 shutdown
|
||||
if result.returncode != 0:
|
||||
return True, DeviceStatus.OFFLINE
|
||||
i = 0
|
||||
while i < self.wait_time * 10:
|
||||
status = await self.get_status(idx)
|
||||
if status == DeviceStatus.ERROR or status == DeviceStatus.UNKNOWN:
|
||||
return False, status
|
||||
if status == DeviceStatus.OFFLINE:
|
||||
return True, DeviceStatus.OFFLINE
|
||||
await asyncio.sleep(0.1)
|
||||
i += 1
|
||||
|
||||
return False, DeviceStatus.UNKNOWN
|
||||
|
||||
async def get_status(self, idx: str, data: str | None = None) -> int:
|
||||
if not data:
|
||||
OK, result_str = await self.get_device_info(idx)
|
||||
self.logger.debug(f"获取状态结果{result_str}")
|
||||
else:
|
||||
OK, result_str = True, data
|
||||
|
||||
try:
|
||||
result_json = json.loads(result_str)
|
||||
|
||||
if OK:
|
||||
if result_json["is_android_started"]:
|
||||
return DeviceStatus.STARTING
|
||||
elif result_json["is_process_started"]:
|
||||
return DeviceStatus.ONLINE
|
||||
else:
|
||||
return DeviceStatus.OFFLINE
|
||||
|
||||
else:
|
||||
if result_json["errmsg"] == "unknown error":
|
||||
return DeviceStatus.UNKNOWN
|
||||
else:
|
||||
return DeviceStatus.ERROR
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"JSON解析错误: {e}")
|
||||
return DeviceStatus.UNKNOWN
|
||||
|
||||
async def get_device_info(self, idx: str) -> tuple[bool, str]:
|
||||
result = self.runner.run(
|
||||
"info",
|
||||
"-v",
|
||||
idx,
|
||||
)
|
||||
self.logger.debug(f"获取模拟器{idx}信息: {result}")
|
||||
if result.returncode != 0:
|
||||
return False, result.stdout.strip()
|
||||
else:
|
||||
return True, result.stdout.strip()
|
||||
|
||||
async def _get_all_info(self) -> str:
|
||||
result = self.runner.run(
|
||||
"info",
|
||||
"-v",
|
||||
"all",
|
||||
)
|
||||
# self.logger.debug(f"result{result.stdout.strip()}")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"命令执行失败: {result}")
|
||||
return result.stdout.strip()
|
||||
|
||||
async def get_all_info(self) -> dict[str, dict[str, str]]:
|
||||
json_data = await self._get_all_info()
|
||||
data = json.loads(json_data)
|
||||
|
||||
result: dict[str, dict[str, str]] = {}
|
||||
|
||||
if not data:
|
||||
return result
|
||||
|
||||
if isinstance(data, dict) and "index" in data and "name" in data:
|
||||
index = data["index"]
|
||||
name = data["name"]
|
||||
status = self.get_status(index, json_data)
|
||||
result[index] = {
|
||||
"title": name,
|
||||
"status": str(status),
|
||||
}
|
||||
|
||||
elif isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict) and "index" in value and "name" in value:
|
||||
index = value["index"]
|
||||
name = value["name"]
|
||||
status = await self.get_status(index)
|
||||
result[index] = {
|
||||
"title": name,
|
||||
"status": str(status),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
async def hide_device(self, idx: str) -> tuple[bool, int]:
|
||||
"""隐藏设备窗口"""
|
||||
status = await self.get_status(idx)
|
||||
if status != DeviceStatus.ONLINE:
|
||||
return False, status
|
||||
result = self.runner.run(
|
||||
"control",
|
||||
"-v",
|
||||
idx,
|
||||
"hide_window",
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return False, status
|
||||
return True, DeviceStatus.ONLINE
|
||||
|
||||
async def show_device(self, idx: str) -> tuple[bool, int]:
|
||||
"""显示设备窗口"""
|
||||
status = await self.get_status(idx)
|
||||
if status != DeviceStatus.ONLINE:
|
||||
return False, status
|
||||
result = self.runner.run(
|
||||
"control",
|
||||
"-v",
|
||||
idx,
|
||||
"show_window",
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return False, status
|
||||
return True, DeviceStatus.ONLINE
|
||||
104
app/utils/device_manager/utils.py
Normal file
104
app/utils/device_manager/utils.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import subprocess
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class DeviceStatus(IntEnum):
|
||||
ONLINE = 0
|
||||
"""设备在线"""
|
||||
OFFLINE = 1
|
||||
"""设备离线"""
|
||||
STARTING = 2
|
||||
"""设备开启中"""
|
||||
CLOSEING = 3
|
||||
"""设备关闭中"""
|
||||
ERROR = 4
|
||||
"""错误"""
|
||||
NOT_FOUND = 5
|
||||
"""未找到设备"""
|
||||
UNKNOWN = 10
|
||||
|
||||
|
||||
class ExeRunner:
|
||||
def __init__(self, exe_path, encoding) -> None:
|
||||
"""
|
||||
指定 exe 路径
|
||||
!请传入绝对路径,使用/分隔路径
|
||||
"""
|
||||
if not os.path.isfile(exe_path):
|
||||
raise FileNotFoundError(f"找不到文件: {exe_path}")
|
||||
self.exe_path = os.path.abspath(exe_path) # 转为绝对路径
|
||||
self.encoding = encoding
|
||||
|
||||
def run(self, *args) -> subprocess.CompletedProcess[str]:
|
||||
"""
|
||||
执行命令,返回结果
|
||||
"""
|
||||
cmd = [self.exe_path] + list(args)
|
||||
print(f"执行: {' '.join(cmd)}")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding=self.encoding,
|
||||
errors="replace",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class BaseDevice(ABC):
|
||||
@abstractmethod
|
||||
async def start(self, idx: str, package_name: str) -> tuple[bool, int, dict]:
|
||||
"""
|
||||
启动设备
|
||||
返回值: (是否成功, 状态码, 启动信息)
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def close(self, idx: str) -> tuple[bool, int]:
|
||||
"""
|
||||
关闭设备或服务
|
||||
返回值: (是否成功, 状态码)
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def get_status(self, idx: str) -> int:
|
||||
"""
|
||||
获取指定模拟器当前状态
|
||||
返回值: 状态码
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def hide_device(self, idx: str) -> tuple[bool, int]:
|
||||
"""
|
||||
隐藏设备窗口
|
||||
返回值: (是否成功, 状态码)
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def show_device(self, idx: str) -> tuple[bool, int]:
|
||||
"""
|
||||
显示设备窗口
|
||||
返回值: (是否成功, 状态码)
|
||||
"""
|
||||
...
|
||||
|
||||
async def get_all_info(self) -> dict[str, dict[str, str]]:
|
||||
"""
|
||||
获取设备信息
|
||||
返回值: 设备字典
|
||||
结构示例:
|
||||
{
|
||||
"0":{
|
||||
"title": 模拟器名字,
|
||||
"status": "1"
|
||||
}
|
||||
}
|
||||
"""
|
||||
...
|
||||
70
app/utils/logger.py
Normal file
70
app/utils/logger.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 MoeSnowyFox
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
from loguru import logger as _logger
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
(Path.cwd() / "debug").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
_logger.remove()
|
||||
|
||||
|
||||
_logger.add(
|
||||
sink=Path.cwd() / "debug/app.log",
|
||||
level="INFO",
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{extra[module]}</cyan> | <level>{message}</level>",
|
||||
enqueue=True,
|
||||
backtrace=True,
|
||||
diagnose=True,
|
||||
rotation="1 week",
|
||||
retention="1 month",
|
||||
compression="zip",
|
||||
)
|
||||
|
||||
_logger.add(
|
||||
sink=sys.stderr,
|
||||
level="DEBUG",
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{extra[module]}</cyan> | <level>{message}</level>",
|
||||
enqueue=True,
|
||||
backtrace=True,
|
||||
diagnose=True,
|
||||
colorize=True,
|
||||
)
|
||||
|
||||
|
||||
_logger = _logger.patch(lambda record: record["extra"].setdefault("module", "未知模块"))
|
||||
|
||||
|
||||
def get_logger(module_name: str):
|
||||
"""
|
||||
获取一个绑定 module 名的日志器
|
||||
|
||||
:param module_name: 模块名称, 如 "用户管理"
|
||||
:return: 绑定后的 logger
|
||||
"""
|
||||
return _logger.bind(module=module_name)
|
||||
|
||||
|
||||
__all__ = ["get_logger"]
|
||||
@@ -1,109 +0,0 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA打包程序
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def version_text(version_numb: list) -> str:
|
||||
"""将版本号列表转为可读的文本信息"""
|
||||
|
||||
if version_numb[3] == 0:
|
||||
version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}"
|
||||
else:
|
||||
version = (
|
||||
f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}"
|
||||
)
|
||||
return version
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
root_path = Path(sys.argv[0]).resolve().parent
|
||||
|
||||
with (root_path / "resources/version.json").open(mode="r", encoding="utf-8") as f:
|
||||
version = json.load(f)
|
||||
|
||||
main_version_numb = list(map(int, version["main_version"].split(".")))
|
||||
updater_version_numb = list(map(int, version["updater_version"].split(".")))
|
||||
|
||||
print("Packaging AUTO_MAA main program ...")
|
||||
|
||||
os.system(
|
||||
"powershell -Command python -m nuitka --standalone --onefile --mingw64"
|
||||
" --enable-plugins=pyside6 --windows-console-mode=disable"
|
||||
" --onefile-tempdir-spec='{TEMP}\\AUTO_MAA'"
|
||||
" --windows-icon-from-ico=resources\\icons\\AUTO_MAA.ico"
|
||||
" --company-name='AUTO_MAA Team' --product-name=AUTO_MAA"
|
||||
f" --file-version={version["main_version"]}"
|
||||
f" --product-version={version["main_version"]}"
|
||||
" --file-description='AUTO_MAA Component'"
|
||||
" --copyright='Copyright © 2024 DLmaster361'"
|
||||
" --assume-yes-for-downloads --output-filename=AUTO_MAA"
|
||||
" --remove-output main.py"
|
||||
)
|
||||
|
||||
print("AUTO_MAA main program packaging completed !")
|
||||
|
||||
shutil.copy(root_path / "app/utils/Updater.py", root_path)
|
||||
|
||||
file_content = (root_path / "Updater.py").read_text(encoding="utf-8")
|
||||
|
||||
(root_path / "Updater.py").write_text(
|
||||
file_content.replace(
|
||||
"from .version import version_text", "from app import version_text"
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print("Packaging AUTO_MAA update program ...")
|
||||
|
||||
os.system(
|
||||
"powershell -Command python -m nuitka --standalone --onefile --mingw64"
|
||||
" --enable-plugins=pyside6 --windows-console-mode=disable"
|
||||
" --onefile-tempdir-spec='{TEMP}\\AUTO_MAA_Updater'"
|
||||
" --windows-icon-from-ico=resources\\icons\\AUTO_MAA_Updater.ico"
|
||||
" --company-name='AUTO_MAA Team' --product-name=AUTO_MAA"
|
||||
f" --file-version={version["updater_version"]}"
|
||||
f" --product-version={version["main_version"]}"
|
||||
" --file-description='AUTO_MAA Component'"
|
||||
" --copyright='Copyright © 2024 DLmaster361'"
|
||||
" --assume-yes-for-downloads --output-filename=Updater"
|
||||
" --remove-output Updater.py"
|
||||
)
|
||||
|
||||
print("AUTO_MAA update program packaging completed !")
|
||||
|
||||
(root_path / "Updater.py").unlink()
|
||||
|
||||
(root_path / "version_info.txt").write_text(
|
||||
f"{version_text(main_version_numb)}\n{version_text(updater_version_numb)}{version["announcement"]}",
|
||||
encoding="utf-8",
|
||||
)
|
||||
70
app/utils/security.py
Normal file
70
app/utils/security.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import base64
|
||||
import win32crypt
|
||||
|
||||
|
||||
def dpapi_encrypt(
|
||||
note: str, description: None | str = None, entropy: None | bytes = None
|
||||
) -> str:
|
||||
"""
|
||||
使用Windows DPAPI加密数据
|
||||
|
||||
:param note: 数据明文
|
||||
:type note: str
|
||||
:param description: 描述信息
|
||||
:type description: str
|
||||
:param entropy: 随机熵
|
||||
:type entropy: bytes
|
||||
:return: 加密后的数据
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
if note == "":
|
||||
return ""
|
||||
|
||||
encrypted = win32crypt.CryptProtectData(
|
||||
note.encode("utf-8"), description, entropy, None, None, 0
|
||||
)
|
||||
return base64.b64encode(encrypted).decode("utf-8")
|
||||
|
||||
|
||||
def dpapi_decrypt(note: str, entropy: None | bytes = None) -> str:
|
||||
"""
|
||||
使用Windows DPAPI解密数据
|
||||
|
||||
:param note: 数据密文
|
||||
:type note: str
|
||||
:param entropy: 随机熵
|
||||
:type entropy: bytes
|
||||
:return: 解密后的明文
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
if note == "":
|
||||
return ""
|
||||
|
||||
decrypted = win32crypt.CryptUnprotectData(
|
||||
base64.b64decode(note), entropy, None, None, 0
|
||||
)
|
||||
return decrypted[1].decode("utf-8")
|
||||
411
docs/Backend_Task_Scheduling_and_WebSocket_Messages.md
Normal file
411
docs/Backend_Task_Scheduling_and_WebSocket_Messages.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# AUTO-MAS 后端任务调度逻辑与WebSocket消息格式说明
|
||||
|
||||
## 1. 任务调度架构概览
|
||||
|
||||
AUTO-MAS 后端采用基于 AsyncIO 的异步任务调度系统,主要由以下核心组件构成:
|
||||
|
||||
### 1.1 核心组件
|
||||
|
||||
- **TaskManager**: 任务调度器,负责任务的创建、运行、停止和清理
|
||||
- **Broadcast**: 消息广播系统,负责在不同组件间传递消息
|
||||
- **WebSocket**: 与前端的实时通信通道
|
||||
- **Config**: 配置管理系统,包含脚本配置、队列配置等
|
||||
|
||||
### 1.2 任务类型
|
||||
|
||||
系统支持三种主要任务模式:
|
||||
|
||||
1. **设置脚本** - 直接执行单个脚本配置
|
||||
2. **自动代理** - 按队列顺序自动执行多个脚本
|
||||
3. **人工排查** - 手动排查和执行任务
|
||||
|
||||
## 2. 任务调度流程
|
||||
|
||||
### 2.1 任务创建流程
|
||||
|
||||
```
|
||||
前端请求 → API接口 → TaskManager.add_task() → 任务验证 → 创建异步任务 → 返回任务ID
|
||||
```
|
||||
|
||||
**具体步骤:**
|
||||
|
||||
1. **任务验证**: 根据模式和UID验证任务配置是否存在
|
||||
2. **重复检查**: 确保相同任务未在运行中
|
||||
3. **任务创建**: 使用`asyncio.create_task()`创建异步任务
|
||||
4. **回调设置**: 添加任务完成回调用于清理工作
|
||||
|
||||
### 2.2 任务执行流程
|
||||
|
||||
#### 设置脚本模式
|
||||
```
|
||||
获取脚本配置 → 确定脚本类型(MAA/General) → 创建对应Manager → 执行任务
|
||||
```
|
||||
|
||||
#### 自动代理模式
|
||||
```
|
||||
获取队列配置 → 构建任务列表 → 逐个执行脚本 → 更新状态 → 发送完成信号
|
||||
```
|
||||
|
||||
### 2.3 任务状态管理
|
||||
|
||||
- **等待**: 任务已加入队列但未开始执行
|
||||
- **运行**: 任务正在执行中
|
||||
- **跳过**: 任务因重复或其他原因被跳过
|
||||
- **完成**: 任务执行完毕
|
||||
|
||||
## 3. WebSocket 消息系统
|
||||
|
||||
### 3.1 消息基础结构
|
||||
|
||||
所有WebSocket消息都遵循统一的JSON格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "消息ID或任务ID",
|
||||
"type": "消息类型",
|
||||
"data": {
|
||||
"具体数据": "根据类型而定"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 消息类型详解
|
||||
|
||||
#### 3.2.1 Update 类型 - 数据更新
|
||||
|
||||
**用途**: 通知前端更新界面数据,"user_list"仅给出当前处于`运行`状态的脚本的用户列表值
|
||||
|
||||
**常见数据格式:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Update",
|
||||
"data": {
|
||||
"user_list": [
|
||||
{
|
||||
"name": "用户名",
|
||||
"status": "运行状态",
|
||||
"config": "配置信息"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Update",
|
||||
"data": {
|
||||
"task_dict": [
|
||||
{
|
||||
"script_id": "脚本ID",
|
||||
"status": "等待/运行/完成/跳过",
|
||||
"name": "脚本名称",
|
||||
"user_list": [
|
||||
{
|
||||
"name": "用户名",
|
||||
"status": "运行状态",
|
||||
"config": "配置信息"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Update",
|
||||
"data": {
|
||||
"task_list": [
|
||||
{
|
||||
"script_id": "脚本ID",
|
||||
"status": "等待/运行/完成/跳过",
|
||||
"name": "脚本名称"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Update",
|
||||
"data": {
|
||||
"log": "任务执行日志内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 Info 类型 - 信息显示
|
||||
|
||||
**用途**: 向前端发送需要显示的信息,包括普通信息、警告和错误
|
||||
|
||||
**数据格式:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Info",
|
||||
"data": {
|
||||
"Error": "错误信息内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Info",
|
||||
"data": {
|
||||
"Warning": "警告信息内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Info",
|
||||
"data": {
|
||||
"Info": "普通信息内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.3 Message 类型 - 对话框请求
|
||||
|
||||
**用途**: 请求前端弹出对话框显示重要信息
|
||||
|
||||
**数据格式:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Message",
|
||||
"data": {
|
||||
"title": "对话框标题",
|
||||
"content": "对话框内容",
|
||||
"type": "info/warning/error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.4 Signal 类型 - 程序信号
|
||||
|
||||
**用途**: 发送程序控制信号和状态通知
|
||||
|
||||
**常见信号:**
|
||||
|
||||
**任务完成信号:**
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Signal",
|
||||
"data": {
|
||||
"Accomplish": "任务完成后调度台显示的日志内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**电源操作信号:**
|
||||
```json
|
||||
{
|
||||
"id": "task-uuid",
|
||||
"type": "Signal",
|
||||
"data": {
|
||||
"power": "NoAction/KillSelf/Sleep/Hibernate/Shutdown/ShutdownForce",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**心跳信号:**
|
||||
```json
|
||||
{
|
||||
"id": "Main",
|
||||
"type": "Signal",
|
||||
"data": {
|
||||
"Ping": "无描述"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "Main",
|
||||
"type": "Signal",
|
||||
"data": {
|
||||
"Pong": "无描述"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 任务管理器详细说明
|
||||
|
||||
### 4.1 TaskManager 核心方法
|
||||
|
||||
#### add_task(mode: str, uid: str)
|
||||
- **功能**: 添加新任务到调度队列
|
||||
- **参数**:
|
||||
- `mode`: 任务模式 ("设置脚本", "自动代理", "人工排查")
|
||||
- `uid`: 任务唯一标识符
|
||||
- **返回**: 任务UUID
|
||||
|
||||
#### stop_task(task_id: str)
|
||||
- **功能**: 停止指定任务
|
||||
- **参数**:
|
||||
- `task_id`: 任务ID,支持 "ALL" 停止所有任务
|
||||
|
||||
#### run_task(mode: str, task_id: UUID, actual_id: Optional[UUID])
|
||||
- **功能**: 执行具体任务逻辑
|
||||
- **流程**: 根据模式选择相应的执行策略
|
||||
|
||||
### 4.2 任务执行器
|
||||
|
||||
#### GeneralManager
|
||||
- **用途**: 处理通用脚本任务
|
||||
- **特点**: 支持自定义脚本路径和参数
|
||||
- **配置**: 基于 GeneralConfig 和 GeneralUserConfig
|
||||
|
||||
#### MaaManager
|
||||
- **用途**: 处理MAA (明日方舟助手) 专用任务
|
||||
- **特点**: 支持模拟器控制、ADB连接、游戏自动化
|
||||
- **配置**: 基于 MaaConfig 和 MaaUserConfig
|
||||
|
||||
## 5. 消息广播系统
|
||||
|
||||
### 5.1 Broadcast 机制
|
||||
|
||||
- **设计模式**: 发布-订阅模式
|
||||
- **功能**: 实现组件间解耦的消息传递
|
||||
- **特点**: 支持多个订阅者同时接收消息
|
||||
|
||||
### 5.2 消息流向
|
||||
|
||||
```
|
||||
任务执行器 → Broadcast → WebSocket → 前端界面
|
||||
↓
|
||||
其他订阅者
|
||||
```
|
||||
|
||||
## 6. 配置管理系统
|
||||
|
||||
### 6.1 配置类型
|
||||
|
||||
- **ScriptConfig**: 脚本配置,包含MAA和General两种类型
|
||||
- **QueueConfig**: 队列配置,定义自动代理任务的执行顺序
|
||||
- **GlobalConfig**: 全局系统配置
|
||||
|
||||
### 6.2 配置操作
|
||||
|
||||
- **锁机制**: 防止配置在使用时被修改
|
||||
- **实时更新**: 支持动态加载配置变更
|
||||
- **类型验证**: 确保配置数据的正确性
|
||||
|
||||
## 7. API 接口说明
|
||||
|
||||
### 7.1 任务控制接口
|
||||
|
||||
**创建任务**
|
||||
- **端点**: `POST /api/dispatch/start`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"mode": "自动代理|人工排查|设置脚本",
|
||||
"taskId": "目标任务ID"
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"status": "success",
|
||||
"message": "操作成功",
|
||||
"websocketId": "新任务ID"
|
||||
}
|
||||
```
|
||||
|
||||
**停止任务**
|
||||
- **端点**: `POST /api/dispatch/stop`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"taskId": "要停止的任务ID"
|
||||
}
|
||||
```
|
||||
|
||||
**电源操作**
|
||||
- **端点**: `POST /api/dispatch/power`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"signal": "NoAction|Shutdown|ShutdownForce|Hibernate|Sleep|KillSelf"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 WebSocket 连接
|
||||
|
||||
**端点**: `WS /api/core/ws`
|
||||
|
||||
**连接特性**:
|
||||
- 同时只允许一个WebSocket连接
|
||||
- 自动心跳检测 (15秒超时)
|
||||
- 连接断开时自动清理资源
|
||||
|
||||
## 8. 错误处理机制
|
||||
|
||||
### 8.1 异常类型
|
||||
|
||||
- **ValueError**: 配置验证失败
|
||||
- **RuntimeError**: 任务状态冲突
|
||||
- **TimeoutError**: 操作超时
|
||||
- **ConnectionError**: 连接相关错误
|
||||
|
||||
### 8.2 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "相关任务ID",
|
||||
"type": "Info",
|
||||
"data": {
|
||||
"Error": "具体错误描述"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 性能和监控
|
||||
|
||||
### 9.1 日志系统
|
||||
|
||||
- **分层日志**: 按模块划分日志记录器
|
||||
- **实时监控**: 支持日志实时推送到前端
|
||||
- **文件轮转**: 自动管理日志文件大小
|
||||
|
||||
### 9.2 资源管理
|
||||
|
||||
- **进程管理**: 自动清理子进程
|
||||
- **内存监控**: 防止内存泄漏
|
||||
- **连接池**: 复用数据库和网络连接
|
||||
|
||||
## 10. 安全考虑
|
||||
|
||||
### 10.1 输入验证
|
||||
|
||||
- **参数校验**: 使用 Pydantic 模型验证
|
||||
- **路径安全**: 防止路径遍历攻击
|
||||
- **命令注入**: 严格控制执行的命令参数
|
||||
|
||||
### 10.2 权限控制
|
||||
|
||||
- **单一连接**: 限制WebSocket连接数量
|
||||
- **操作限制**: 防止重复或冲突操作
|
||||
- **资源保护**: 防止资源滥用
|
||||
|
||||
---
|
||||
|
||||
*此文档基于 AUTO-MAS v5.0.0 版本编写,详细的API文档和配置说明请参考相关配置文件和源代码注释。*
|
||||
126
docs/TaskManager_WebSocket_Implementation.md
Normal file
126
docs/TaskManager_WebSocket_Implementation.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# TaskManager WebSocket消息处理功能实现
|
||||
|
||||
## 功能概述
|
||||
|
||||
根据后端TaskManager的WebSocket消息机制,实现了前端对ID为"TaskManager"的WebSocket消息的完整处理逻辑。
|
||||
|
||||
## 后端TaskManager消息分析
|
||||
|
||||
### 消息格式
|
||||
```json
|
||||
{
|
||||
"id": "TaskManager",
|
||||
"type": "Signal",
|
||||
"data": {
|
||||
"newTask": "任务UUID"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 触发时机
|
||||
当后端启动时运行的队列开始执行时,TaskManager会发送此消息通知前端有新任务被自动创建。
|
||||
|
||||
## 前端实现
|
||||
|
||||
### 1. WebSocket订阅机制
|
||||
在`useSchedulerLogic.ts`中添加了以下功能:
|
||||
|
||||
- **subscribeToTaskManager()**: 订阅ID为"TaskManager"的WebSocket消息
|
||||
- **handleTaskManagerMessage()**: 处理TaskManager发送的消息
|
||||
- **createSchedulerTabForTask()**: 根据任务ID自动创建调度台
|
||||
|
||||
### 2. 自动调度台创建逻辑
|
||||
|
||||
当收到`newTask`信号时,系统会:
|
||||
|
||||
1. **检查重复**: 验证是否已存在相同websocketId的调度台
|
||||
2. **创建调度台**: 自动创建新的调度台标签页
|
||||
3. **设置状态**: 直接将调度台状态设置为"运行"
|
||||
4. **建立连接**: 立即订阅该任务的WebSocket消息
|
||||
5. **用户提示**: 显示成功创建的消息提示
|
||||
|
||||
### 3. 调度台特性
|
||||
|
||||
自动创建的调度台具有以下特性:
|
||||
- 标题格式:`自动调度台{编号}`
|
||||
- 初始状态:`运行`
|
||||
- 可关闭:`true`(但运行时不可删除)
|
||||
- 自动订阅:立即开始接收任务消息
|
||||
|
||||
### 4. 生命周期管理
|
||||
|
||||
- **初始化**: 在组件挂载时调用`initialize()`订阅TaskManager消息
|
||||
- **清理**: 在组件卸载时取消TaskManager订阅
|
||||
- **任务结束**: 复用现有的任务结束处理逻辑
|
||||
|
||||
## 代码修改点
|
||||
|
||||
### 1. useSchedulerLogic.ts
|
||||
```typescript
|
||||
// 新增TaskManager消息订阅
|
||||
const subscribeToTaskManager = () => {
|
||||
ws.subscribe('TaskManager', {
|
||||
onMessage: (message) => handleTaskManagerMessage(message)
|
||||
})
|
||||
}
|
||||
|
||||
// 新增TaskManager消息处理
|
||||
const handleTaskManagerMessage = (wsMessage: any) => {
|
||||
if (type === 'Signal' && data && data.newTask) {
|
||||
createSchedulerTabForTask(data.newTask)
|
||||
}
|
||||
}
|
||||
|
||||
// 新增自动调度台创建
|
||||
const createSchedulerTabForTask = (taskId: string) => {
|
||||
// 创建运行状态的调度台并立即订阅
|
||||
}
|
||||
```
|
||||
|
||||
### 2. index.vue
|
||||
```typescript
|
||||
// 生命周期中添加初始化调用
|
||||
onMounted(() => {
|
||||
initialize() // 订阅TaskManager消息
|
||||
loadTaskOptions()
|
||||
})
|
||||
```
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 1. 无缝集成
|
||||
- 完全复用现有的调度台逻辑和UI组件
|
||||
- 与手动创建的调度台行为一致
|
||||
- 支持所有现有功能(日志显示、任务总览、消息处理等)
|
||||
|
||||
### 2. 状态同步
|
||||
- 调度台状态与后端任务状态严格同步
|
||||
- 支持任务完成后的自动状态更新
|
||||
- 正确处理WebSocket连接的建立和清理
|
||||
|
||||
### 3. 用户体验
|
||||
- 自动切换到新创建的调度台
|
||||
- 提供清晰的成功提示
|
||||
- 防止重复创建相同任务的调度台
|
||||
|
||||
### 4. 错误处理
|
||||
- 检查消息格式的有效性
|
||||
- 防止重复订阅和创建
|
||||
- 优雅处理异常情况
|
||||
|
||||
## 测试验证
|
||||
|
||||
功能实现后需要验证以下场景:
|
||||
|
||||
1. **启动时队列**: 后端启动时运行的队列应自动创建调度台
|
||||
2. **消息接收**: 调度台应正确接收和显示任务消息
|
||||
3. **状态更新**: 任务状态变化应正确反映在UI上
|
||||
4. **任务结束**: 任务完成后应正确清理资源
|
||||
5. **重复处理**: 相同任务不应创建多个调度台
|
||||
|
||||
## 兼容性
|
||||
|
||||
- 完全向后兼容现有功能
|
||||
- 不影响手动创建的调度台
|
||||
- 保持现有的WebSocket消息处理机制
|
||||
- 复用所有现有的UI组件和样式
|
||||
427
docs/useWebSocket_API_Reference.md
Normal file
427
docs/useWebSocket_API_Reference.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# useWebSocket API 参考文档
|
||||
|
||||
## 概述
|
||||
|
||||
`useWebSocket()` 组合式函数提供了完整的 WebSocket 通信接口,包含消息订阅、连接管理、状态监控等核心功能。
|
||||
|
||||
## 导出函数详解
|
||||
|
||||
### 1. subscribe() - 订阅消息
|
||||
|
||||
```typescript
|
||||
const subscribe = (
|
||||
filter: SubscriptionFilter,
|
||||
handler: (message: WebSocketBaseMessage) => void
|
||||
): string
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
|
||||
**filter: SubscriptionFilter**
|
||||
```typescript
|
||||
interface SubscriptionFilter {
|
||||
type?: string // 消息类型过滤器(可选)
|
||||
id?: string // 消息ID过滤器(可选)
|
||||
needCache?: boolean // 是否启用缓存回放(可选)
|
||||
}
|
||||
```
|
||||
|
||||
**handler: Function**
|
||||
- 消息处理回调函数
|
||||
- 参数: `message: WebSocketBaseMessage`
|
||||
- 无返回值
|
||||
|
||||
#### 返回值
|
||||
- `string`: 唯一的订阅ID,用于后续取消订阅
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// 订阅所有消息
|
||||
const allMsgSub = subscribe({}, (msg) => {
|
||||
console.log('收到消息:', msg)
|
||||
})
|
||||
|
||||
// 订阅特定类型消息
|
||||
const taskSub = subscribe(
|
||||
{ type: 'TaskUpdate', needCache: true },
|
||||
(msg) => {
|
||||
console.log('任务更新:', msg.data)
|
||||
}
|
||||
)
|
||||
|
||||
// 订阅特定ID消息
|
||||
const specificSub = subscribe(
|
||||
{ id: 'TaskManager' },
|
||||
(msg) => {
|
||||
console.log('任务管理器消息:', msg)
|
||||
}
|
||||
)
|
||||
|
||||
// 精确订阅(同时匹配type和id)
|
||||
const preciseSub = subscribe(
|
||||
{ type: 'Progress', id: 'task_001', needCache: true },
|
||||
(msg) => {
|
||||
console.log('特定任务进度:', msg.data)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### 特殊功能
|
||||
- **自动回放**: 如果 `needCache: true`,会立即回放匹配的历史消息
|
||||
- **过滤优先级**: type + id > type > id > 全部
|
||||
- **引用计数**: 多个订阅共享缓存,自动管理内存
|
||||
|
||||
---
|
||||
|
||||
### 2. unsubscribe() - 取消订阅
|
||||
|
||||
```typescript
|
||||
const unsubscribe = (subscriptionId: string): void
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
|
||||
**subscriptionId: string**
|
||||
- 由 `subscribe()` 返回的订阅ID
|
||||
- 必须是有效的订阅ID
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
const subId = subscribe({ type: 'TaskUpdate' }, handleTaskUpdate)
|
||||
|
||||
// 取消订阅
|
||||
unsubscribe(subId)
|
||||
|
||||
// Vue 组件中的最佳实践
|
||||
import { onUnmounted } from 'vue'
|
||||
|
||||
const setupSubscription = () => {
|
||||
const subId = subscribe({ type: 'TaskUpdate' }, handleTaskUpdate)
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribe(subId)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 自动清理
|
||||
- 自动减少缓存引用计数
|
||||
- 引用计数为0时清理相关缓存
|
||||
- 不会影响其他订阅者
|
||||
|
||||
---
|
||||
|
||||
### 3. sendRaw() - 发送消息
|
||||
|
||||
```typescript
|
||||
const sendRaw = (type: string, data?: any, id?: string): void
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
|
||||
**type: string** (必需)
|
||||
- 消息类型标识
|
||||
- 后端用于路由消息
|
||||
|
||||
**data: any** (可选)
|
||||
- 消息负载数据
|
||||
- 可以是任何可序列化的对象
|
||||
|
||||
**id: string** (可选)
|
||||
- 消息标识符
|
||||
- 用于消息跟踪和响应匹配
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// 发送简单消息
|
||||
sendRaw('Hello')
|
||||
|
||||
// 发送带数据的消息
|
||||
sendRaw('TaskStart', {
|
||||
taskId: '12345',
|
||||
config: { timeout: 30000 }
|
||||
})
|
||||
|
||||
// 发送带ID的消息(便于追踪响应)
|
||||
sendRaw('GetTaskStatus', { taskId: '12345' }, 'query_001')
|
||||
|
||||
// 发送控制信号
|
||||
sendRaw('Signal', {
|
||||
command: 'pause',
|
||||
reason: '用户手动暂停'
|
||||
}, 'TaskManager')
|
||||
```
|
||||
|
||||
#### 发送条件
|
||||
- 仅在 WebSocket 连接为 `OPEN` 状态时发送
|
||||
- 连接异常时静默失败(不抛出异常)
|
||||
- 自动JSON序列化
|
||||
|
||||
---
|
||||
|
||||
### 4. getConnectionInfo() - 获取连接信息
|
||||
|
||||
```typescript
|
||||
const getConnectionInfo = (): ConnectionInfo
|
||||
```
|
||||
|
||||
#### 返回值类型
|
||||
|
||||
```typescript
|
||||
interface ConnectionInfo {
|
||||
connectionId: string // 连接唯一标识
|
||||
status: WebSocketStatus // 当前连接状态
|
||||
subscriberCount: number // 当前订阅者数量
|
||||
moduleLoadCount: number // 模块加载计数
|
||||
wsReadyState: number | null // WebSocket原生状态
|
||||
isConnecting: boolean // 是否正在连接
|
||||
hasHeartbeat: boolean // 是否启用心跳
|
||||
hasEverConnected: boolean // 是否曾经连接成功
|
||||
reconnectAttempts: number // 重连尝试次数
|
||||
isPersistentMode: boolean // 是否持久化模式
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
const info = getConnectionInfo()
|
||||
|
||||
console.log('连接ID:', info.connectionId)
|
||||
console.log('连接状态:', info.status)
|
||||
console.log('订阅者数量:', info.subscriberCount)
|
||||
|
||||
// 检查连接是否健康
|
||||
const isHealthy = info.status === '已连接' &&
|
||||
info.hasHeartbeat &&
|
||||
info.wsReadyState === WebSocket.OPEN
|
||||
|
||||
// 监控重连情况
|
||||
if (info.reconnectAttempts > 0) {
|
||||
console.log(`已重连 ${info.reconnectAttempts} 次`)
|
||||
}
|
||||
```
|
||||
|
||||
#### 调试用途
|
||||
- 诊断连接问题
|
||||
- 监控连接质量
|
||||
- 统计使用情况
|
||||
|
||||
---
|
||||
|
||||
### 5. status - 连接状态
|
||||
|
||||
```typescript
|
||||
const status: Ref<WebSocketStatus>
|
||||
```
|
||||
|
||||
#### 状态类型
|
||||
|
||||
```typescript
|
||||
type WebSocketStatus = '连接中' | '已连接' | '已断开' | '连接错误'
|
||||
```
|
||||
|
||||
#### 状态说明
|
||||
|
||||
| 状态 | 描述 | 触发条件 |
|
||||
|------|------|----------|
|
||||
| `'连接中'` | 正在建立连接 | WebSocket.CONNECTING |
|
||||
| `'已连接'` | 连接成功 | WebSocket.OPEN |
|
||||
| `'已断开'` | 连接断开 | WebSocket.CLOSED |
|
||||
| `'连接错误'` | 连接异常 | WebSocket.onerror |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
import { watch } from 'vue'
|
||||
|
||||
const { status } = useWebSocket()
|
||||
|
||||
// 监听状态变化
|
||||
watch(status, (newStatus) => {
|
||||
console.log('连接状态变化:', newStatus)
|
||||
|
||||
switch (newStatus) {
|
||||
case '已连接':
|
||||
console.log('✅ WebSocket 连接成功')
|
||||
break
|
||||
case '已断开':
|
||||
console.log('❌ WebSocket 连接断开')
|
||||
break
|
||||
case '连接错误':
|
||||
console.log('⚠️ WebSocket 连接错误')
|
||||
break
|
||||
case '连接中':
|
||||
console.log('🔄 WebSocket 连接中...')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// 在模板中显示状态
|
||||
// <div>连接状态: {{ status }}</div>
|
||||
```
|
||||
|
||||
#### 响应式特性
|
||||
- Vue 响应式 Ref 对象
|
||||
- 自动更新 UI
|
||||
- 可用于计算属性和监听器
|
||||
|
||||
---
|
||||
|
||||
### 6. backendStatus - 后端状态
|
||||
|
||||
```typescript
|
||||
const backendStatus: Ref<BackendStatus>
|
||||
```
|
||||
|
||||
#### 状态类型
|
||||
|
||||
```typescript
|
||||
type BackendStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error'
|
||||
```
|
||||
|
||||
#### 状态说明
|
||||
|
||||
| 状态 | 描述 | 含义 |
|
||||
|------|------|------|
|
||||
| `'unknown'` | 未知状态 | 初始状态,尚未检测 |
|
||||
| `'starting'` | 启动中 | 后端服务正在启动 |
|
||||
| `'running'` | 运行中 | 后端服务正常运行 |
|
||||
| `'stopped'` | 已停止 | 后端服务已停止 |
|
||||
| `'error'` | 错误状态 | 后端服务异常 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
const { backendStatus, restartBackend } = useWebSocket()
|
||||
|
||||
// 监听后端状态
|
||||
watch(backendStatus, (newStatus) => {
|
||||
console.log('后端状态:', newStatus)
|
||||
|
||||
switch (newStatus) {
|
||||
case 'running':
|
||||
console.log('✅ 后端服务运行正常')
|
||||
break
|
||||
case 'stopped':
|
||||
console.log('⏹️ 后端服务已停止')
|
||||
break
|
||||
case 'error':
|
||||
console.log('❌ 后端服务异常')
|
||||
// 可以提示用户或自动重启
|
||||
break
|
||||
case 'starting':
|
||||
console.log('🚀 后端服务启动中...')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// 根据状态显示不同UI
|
||||
const statusColor = computed(() => {
|
||||
switch (backendStatus.value) {
|
||||
case 'running': return 'green'
|
||||
case 'error': return 'red'
|
||||
case 'starting': return 'orange'
|
||||
default: return 'gray'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 自动管理
|
||||
- 每3秒自动检测一次
|
||||
- 异常时自动尝试重启(最多3次)
|
||||
- 集成 Electron 进程管理
|
||||
|
||||
---
|
||||
|
||||
## 完整使用示例
|
||||
|
||||
```typescript
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const {
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
sendRaw,
|
||||
getConnectionInfo,
|
||||
status,
|
||||
backendStatus
|
||||
} = useWebSocket()
|
||||
|
||||
let taskSubscription: string
|
||||
let systemSubscription: string
|
||||
|
||||
onMounted(() => {
|
||||
// 订阅任务消息
|
||||
taskSubscription = subscribe(
|
||||
{ type: 'TaskUpdate', needCache: true },
|
||||
(message) => {
|
||||
console.log('任务更新:', message.data)
|
||||
}
|
||||
)
|
||||
|
||||
// 订阅系统消息
|
||||
systemSubscription = subscribe(
|
||||
{ id: 'System' },
|
||||
(message) => {
|
||||
console.log('系统消息:', message)
|
||||
}
|
||||
)
|
||||
|
||||
// 发送初始化消息
|
||||
sendRaw('ClientReady', {
|
||||
timestamp: Date.now()
|
||||
}, 'System')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理订阅
|
||||
if (taskSubscription) unsubscribe(taskSubscription)
|
||||
if (systemSubscription) unsubscribe(systemSubscription)
|
||||
})
|
||||
|
||||
// 监听连接状态
|
||||
watch([status, backendStatus], ([wsStatus, beStatus]) => {
|
||||
console.log(`WS: ${wsStatus}, Backend: ${beStatus}`)
|
||||
})
|
||||
|
||||
// 获取连接信息
|
||||
const connectionInfo = getConnectionInfo()
|
||||
|
||||
return {
|
||||
status,
|
||||
backendStatus,
|
||||
connectionInfo,
|
||||
sendMessage: (type: string, data: any) => sendRaw(type, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 订阅管理
|
||||
- 总是在组件卸载时取消订阅
|
||||
- 使用 `needCache: true` 确保不丢失消息
|
||||
- 避免重复订阅相同的消息类型
|
||||
|
||||
### 2. 错误处理
|
||||
- 监听连接状态变化
|
||||
- 根据后端状态调整UI显示
|
||||
- 实现重连提示和手动重启
|
||||
|
||||
### 3. 性能优化
|
||||
- 精确的过滤条件减少不必要的处理
|
||||
- 合理使用缓存避免消息丢失
|
||||
- 及时取消不需要的订阅
|
||||
|
||||
### 4. 调试技巧
|
||||
- 使用 `getConnectionInfo()` 诊断问题
|
||||
- 开发环境下查看控制台日志
|
||||
- 监控订阅者数量避免内存泄漏
|
||||
336
docs/useWebSocket_Analysis.md
Normal file
336
docs/useWebSocket_Analysis.md
Normal file
@@ -0,0 +1,336 @@
|
||||
## 核心架构设计
|
||||
|
||||
### 1. 全局持久化存储
|
||||
|
||||
```typescript
|
||||
const WS_STORAGE_KEY = Symbol.for('GLOBAL_WEBSOCKET_PERSISTENT')
|
||||
```
|
||||
|
||||
- 使用 `Symbol.for()` 确保全局唯一性
|
||||
- 存储在 `window` 对象上,实现跨组件共享
|
||||
- 避免多次实例化,确保连接唯一性
|
||||
|
||||
### 2. 状态管理结构
|
||||
|
||||
```typescript
|
||||
interface GlobalWSStorage {
|
||||
wsRef: WebSocket | null // WebSocket 实例
|
||||
status: Ref<WebSocketStatus> // 连接状态
|
||||
subscriptions: Ref<Map<string, WebSocketSubscription>> // 订阅管理
|
||||
cacheMarkers: Ref<Map<string, CacheMarker>> // 缓存标记
|
||||
cachedMessages: Ref<Array<CachedMessage>> // 消息缓存
|
||||
// ... 其他状态
|
||||
}
|
||||
```
|
||||
|
||||
## 核心功能模块
|
||||
|
||||
### 1. 配置管理
|
||||
|
||||
```typescript
|
||||
const BASE_WS_URL = 'ws://localhost:36163/api/core/ws'
|
||||
const HEARTBEAT_INTERVAL = 15000 // 心跳间隔
|
||||
const HEARTBEAT_TIMEOUT = 5000 // 心跳超时
|
||||
const BACKEND_CHECK_INTERVAL = 3000 // 后端检查间隔
|
||||
const MAX_RESTART_ATTEMPTS = 3 // 最大重启尝试次数
|
||||
const RESTART_DELAY = 2000 // 重启延迟
|
||||
const MAX_QUEUE_SIZE = 50 // 最大队列大小
|
||||
const MESSAGE_TTL = 60000 // 消息过期时间
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 所有时间配置使用毫秒为单位
|
||||
- 可根据网络环境调整超时时间
|
||||
- 队列大小限制防止内存泄漏
|
||||
|
||||
### 2. 消息订阅系统
|
||||
|
||||
#### 订阅过滤器
|
||||
```typescript
|
||||
interface SubscriptionFilter {
|
||||
type?: string // 消息类型过滤
|
||||
id?: string // 消息ID过滤
|
||||
needCache?: boolean // 是否需要缓存
|
||||
}
|
||||
```
|
||||
|
||||
#### 订阅机制
|
||||
```typescript
|
||||
export const subscribe = (
|
||||
filter: SubscriptionFilter,
|
||||
handler: (message: WebSocketBaseMessage) => void
|
||||
): string => {
|
||||
// 1. 生成唯一订阅ID
|
||||
// 2. 创建订阅记录
|
||||
// 3. 添加缓存标记
|
||||
// 4. 回放匹配的缓存消息
|
||||
}
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 支持按 `type` 和 `id` 的组合过滤
|
||||
- 自动回放缓存消息,确保不丢失历史数据
|
||||
- 返回订阅ID用于后续取消订阅
|
||||
|
||||
### 3. 智能缓存系统
|
||||
|
||||
#### 缓存标记机制
|
||||
```typescript
|
||||
interface CacheMarker {
|
||||
type?: string
|
||||
id?: string
|
||||
refCount: number // 引用计数
|
||||
}
|
||||
```
|
||||
|
||||
#### 缓存策略
|
||||
- **引用计数**: 订阅时 +1,取消订阅时 -1
|
||||
- **自动清理**: 引用计数为 0 时删除标记
|
||||
- **TTL机制**: 消息超过 60 秒自动过期
|
||||
- **大小限制**: 每个队列最多保留 50 条消息
|
||||
|
||||
|
||||
### 4. 心跳检测机制
|
||||
|
||||
```typescript
|
||||
const startGlobalHeartbeat = (ws: WebSocket) => {
|
||||
global.heartbeatTimer = window.setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
const pingTime = Date.now()
|
||||
global.lastPingTime = pingTime
|
||||
ws.send(JSON.stringify({
|
||||
type: 'Signal',
|
||||
data: { Ping: pingTime, connectionId: global.connectionId }
|
||||
}))
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 5. 后端服务监控
|
||||
|
||||
#### 状态检测
|
||||
```typescript
|
||||
type BackendStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error'
|
||||
```
|
||||
|
||||
#### 自动重启逻辑
|
||||
```typescript
|
||||
const restartBackend = async (): Promise<boolean> => {
|
||||
// 1. 防重入检查
|
||||
// 2. 递增重启计数
|
||||
// 3. 调用 Electron API 启动后端
|
||||
// 4. 更新状态
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 6. 连接控制机制
|
||||
|
||||
#### 连接权限控制
|
||||
```typescript
|
||||
const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连']
|
||||
const checkConnectionPermission = () => getGlobalStorage().allowNewConnection
|
||||
```
|
||||
|
||||
#### 连接锁机制
|
||||
```typescript
|
||||
let isGlobalConnectingLock = false
|
||||
const acquireConnectionLock = () => {
|
||||
if (isGlobalConnectingLock) return false
|
||||
isGlobalConnectingLock = true
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
**AI 开发要点**:
|
||||
- 防止并发连接导致的竞态条件
|
||||
- 只允许特定原因的连接请求
|
||||
- 确保全局唯一连接
|
||||
|
||||
## 消息流处理
|
||||
|
||||
### 1. 消息匹配算法
|
||||
|
||||
```typescript
|
||||
const messageMatchesFilter = (message: WebSocketBaseMessage, filter: SubscriptionFilter): boolean => {
|
||||
// 如果都不指定,匹配所有消息
|
||||
if (!filter.type && !filter.id) return true
|
||||
|
||||
// 如果只指定type
|
||||
if (filter.type && !filter.id) return message.type === filter.type
|
||||
|
||||
// 如果只指定id
|
||||
if (!filter.type && filter.id) return message.id === filter.id
|
||||
|
||||
// 如果同时指定type和id,必须都匹配
|
||||
return message.type === filter.type && message.id === filter.id
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 消息分发流程
|
||||
|
||||
```
|
||||
WebSocket 接收消息
|
||||
↓
|
||||
JSON 解析
|
||||
↓
|
||||
遍历所有订阅者
|
||||
↓
|
||||
匹配过滤条件
|
||||
↓
|
||||
调用处理器函数
|
||||
↓
|
||||
检查是否需要缓存
|
||||
↓
|
||||
添加到缓存队列
|
||||
```
|
||||
|
||||
## 外部接口设计
|
||||
|
||||
### 1. 主要导出函数
|
||||
|
||||
```typescript
|
||||
export function useWebSocket() {
|
||||
return {
|
||||
subscribe, // 订阅消息
|
||||
unsubscribe, // 取消订阅
|
||||
sendRaw, // 发送消息
|
||||
getConnectionInfo, // 获取连接信息
|
||||
status, // 连接状态
|
||||
backendStatus, // 后端状态
|
||||
restartBackend, // 重启后端
|
||||
getBackendStatus, // 获取后端状态
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 特殊接口
|
||||
|
||||
```typescript
|
||||
export const connectAfterBackendStart = async (): Promise<boolean>
|
||||
```
|
||||
- 后端启动后的连接入口
|
||||
- 启动后端监控
|
||||
- 设置连接权限
|
||||
|
||||
## 错误处理策略
|
||||
|
||||
### 1. 连接错误处理
|
||||
- WebSocket 连接失败时设置状态为 '连接错误'
|
||||
- 通过后端监控检测服务状态
|
||||
- 自动重启后端服务
|
||||
|
||||
### 2. 消息处理错误
|
||||
- 订阅处理器异常时记录警告但不中断其他订阅者
|
||||
- JSON 解析失败时静默忽略
|
||||
- 发送消息失败时静默处理
|
||||
|
||||
### 3. 后端故障处理
|
||||
```typescript
|
||||
const handleBackendFailure = async () => {
|
||||
if (global.backendRestartAttempts >= MAX_RESTART_ATTEMPTS) {
|
||||
// 显示错误对话框,提示重启应用
|
||||
Modal.error({
|
||||
title: '后端服务异常',
|
||||
content: '后端服务多次重启失败,请重启整个应用程序。'
|
||||
})
|
||||
return
|
||||
}
|
||||
// 自动重启逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 调试和监控
|
||||
|
||||
### 1. 调试模式
|
||||
```typescript
|
||||
const DEBUG = process.env.NODE_ENV === 'development'
|
||||
const log = (...args: any[]) => {
|
||||
if (DEBUG) console.log('[WebSocket]', ...args)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 连接信息监控
|
||||
```typescript
|
||||
const getConnectionInfo = () => ({
|
||||
connectionId: global.connectionId,
|
||||
status: global.status.value,
|
||||
subscriberCount: global.subscriptions.value.size,
|
||||
moduleLoadCount: global.moduleLoadCount,
|
||||
wsReadyState: global.wsRef ? global.wsRef.readyState : null,
|
||||
isConnecting: global.isConnecting,
|
||||
hasHeartbeat: !!global.heartbeatTimer,
|
||||
hasEverConnected: global.hasEverConnected,
|
||||
reconnectAttempts: global.reconnectAttempts,
|
||||
isPersistentMode: true,
|
||||
})
|
||||
```
|
||||
|
||||
## AI 开发建议
|
||||
|
||||
### 1. 使用模式
|
||||
```typescript
|
||||
// 在 Vue 组件中使用
|
||||
const { subscribe, unsubscribe, sendRaw, status } = useWebSocket()
|
||||
|
||||
// 订阅特定类型消息
|
||||
const subId = subscribe(
|
||||
{ type: 'TaskUpdate', needCache: true },
|
||||
(message) => {
|
||||
console.log('收到任务更新:', message.data)
|
||||
}
|
||||
)
|
||||
|
||||
// 组件卸载时取消订阅
|
||||
onUnmounted(() => {
|
||||
unsubscribe(subId)
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 扩展建议
|
||||
- 添加消息重试机制
|
||||
- 实现消息优先级队列
|
||||
- 支持消息压缩
|
||||
- 添加连接质量监控
|
||||
|
||||
### 3. 性能优化点
|
||||
- 使用 `Object.freeze()` 冻结配置对象
|
||||
- 考虑使用 Web Worker 处理大量消息
|
||||
- 实现消息批处理机制
|
||||
- 添加消息去重功能
|
||||
|
||||
### 4. 安全考虑
|
||||
- 验证消息来源
|
||||
- 实现消息签名机制
|
||||
- 添加连接认证
|
||||
- 防止消息注入攻击
|
||||
|
||||
## 依赖关系
|
||||
|
||||
### 1. 外部依赖
|
||||
- `vue`: 响应式系统和组合式API
|
||||
- `ant-design-vue`: UI组件库(Modal)
|
||||
- `schedulerHandlers`: 默认消息处理器
|
||||
|
||||
### 2. 运行时依赖
|
||||
- `window.electronAPI`: Electron主进程通信
|
||||
- WebSocket API: 浏览器原生支持
|
||||
|
||||
## 总结
|
||||
|
||||
这个 WebSocket 组合式函数是一个功能完整、设计精良的实时通信解决方案。它不仅解决了基本的 WebSocket 连接问题,还提供了高级功能如智能缓存、自动重连、后端监控等。
|
||||
|
||||
**核心优势**:
|
||||
1. 全局持久化连接,避免重复建立
|
||||
2. 智能订阅系统,支持精确过滤
|
||||
3. 自动缓存回放,确保数据完整性
|
||||
4. 完善的错误处理和自动恢复
|
||||
5. 详细的调试和监控信息
|
||||
|
||||
**适用场景**:
|
||||
- 实时数据展示
|
||||
- 任务状态监控
|
||||
- 系统通知推送
|
||||
- 双向通信应用
|
||||
2
frontend/.env
Normal file
2
frontend/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_APP_ENV='prod'
|
||||
VITE_APP_VERSION='1.0.1'
|
||||
2
frontend/.env.development
Normal file
2
frontend/.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_APP_ENV='dev'
|
||||
VITE_APP_VERSION='0.9.0'
|
||||
28
frontend/.gitignore
vendored
Normal file
28
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.kiro
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
dist-electron
|
||||
.yarn
|
||||
.yarnrc.yml
|
||||
3
frontend/.prettierignore
Normal file
3
frontend/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
node_modules
|
||||
dist-electron
|
||||
9
frontend/.prettierrc
Normal file
9
frontend/.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
1
frontend/.yarnrc.yml
Normal file
1
frontend/.yarnrc.yml
Normal file
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
60
frontend/README.md
Normal file
60
frontend/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# AUTO-MAS Frontend
|
||||
|
||||
基于 Vue 3 + TypeScript + Ant Design Vue + Electron 的桌面应用程序。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🎨 使用 Ant Design Vue 组件库
|
||||
- 🌙 支持深色模式(跟随系统/深色/浅色)
|
||||
- 🎨 支持多种主题色切换
|
||||
- 📱 响应式侧边栏布局
|
||||
- 🔧 内置开发者工具
|
||||
- ⚡ 基于 Vite 的快速开发体验
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # 组件
|
||||
│ └── AppLayout.vue # 主布局组件
|
||||
├── views/ # 页面
|
||||
│ ├── Home.vue # 主页
|
||||
│ ├── Scripts.vue # 脚本管理
|
||||
│ ├── Plans.vue # 计划管理
|
||||
│ ├── Queue.vue # 调度队列
|
||||
│ ├── Scheduler.vue # 调度中心
|
||||
│ ├── History.vue # 历史记录
|
||||
│ └── Settings.vue # 设置页面
|
||||
├── router/ # 路由配置
|
||||
├── composables/ # 组合式函数
|
||||
│ └── useTheme.ts # 主题管理
|
||||
└── main.ts # 应用入口
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 开发模式
|
||||
直接打开electron窗口
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### 构建
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端框架**: Vue 3 + TypeScript
|
||||
- **UI 组件库**: Ant Design Vue 4.x
|
||||
- **图标**: @ant-design/icons-vue
|
||||
- **路由**: Vue Router 4
|
||||
- **构建工具**: Vite
|
||||
- **桌面端**: Electron
|
||||
- **包管理**: Yarn
|
||||
1175
frontend/electron/main.ts
Normal file
1175
frontend/electron/main.ts
Normal file
File diff suppressed because it is too large
Load Diff
73
frontend/electron/preload.ts
Normal file
73
frontend/electron/preload.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('预加载脚本已加载')
|
||||
})
|
||||
|
||||
// 暴露安全的 API 给渲染进程
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
openDevTools: () => ipcRenderer.invoke('open-dev-tools'),
|
||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||
selectFile: (filters?: any[]) => ipcRenderer.invoke('select-file', filters),
|
||||
openUrl: (url: string) => ipcRenderer.invoke('open-url', url),
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => ipcRenderer.invoke('window-minimize'),
|
||||
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
|
||||
windowClose: () => ipcRenderer.invoke('window-close'),
|
||||
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
||||
appQuit: () => ipcRenderer.invoke('app-quit'),
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => ipcRenderer.invoke('get-related-processes'),
|
||||
killAllProcesses: () => ipcRenderer.invoke('kill-all-processes'),
|
||||
forceExit: () => ipcRenderer.invoke('force-exit'),
|
||||
|
||||
// 初始化相关API
|
||||
checkEnvironment: () => ipcRenderer.invoke('check-environment'),
|
||||
checkCriticalFiles: () => ipcRenderer.invoke('check-critical-files'),
|
||||
downloadPython: (mirror?: string) => ipcRenderer.invoke('download-python', mirror),
|
||||
installPip: () => ipcRenderer.invoke('install-pip'),
|
||||
downloadGit: () => ipcRenderer.invoke('download-git'),
|
||||
checkGitUpdate: () => ipcRenderer.invoke('check-git-update'),
|
||||
installDependencies: (mirror?: string) => ipcRenderer.invoke('install-dependencies', mirror),
|
||||
cloneBackend: (repoUrl?: string) => ipcRenderer.invoke('clone-backend', repoUrl),
|
||||
updateBackend: (repoUrl?: string) => ipcRenderer.invoke('update-backend', repoUrl),
|
||||
startBackend: () => ipcRenderer.invoke('start-backend'),
|
||||
stopBackend: () => ipcRenderer.invoke('stop-backend'),
|
||||
|
||||
// 管理员权限相关
|
||||
checkAdmin: () => ipcRenderer.invoke('check-admin'),
|
||||
restartAsAdmin: () => ipcRenderer.invoke('restart-as-admin'),
|
||||
|
||||
// 配置文件操作
|
||||
saveConfig: (config: any) => ipcRenderer.invoke('save-config', config),
|
||||
loadConfig: () => ipcRenderer.invoke('load-config'),
|
||||
resetConfig: () => ipcRenderer.invoke('reset-config'),
|
||||
|
||||
// 托盘设置实时更新
|
||||
updateTraySettings: (uiSettings: any) => ipcRenderer.invoke('update-tray-settings', uiSettings),
|
||||
|
||||
// 日志文件操作
|
||||
getLogPath: () => ipcRenderer.invoke('get-log-path'),
|
||||
getLogFiles: () => ipcRenderer.invoke('get-log-files'),
|
||||
getLogs: (lines?: number, fileName?: string) => ipcRenderer.invoke('get-logs', lines, fileName),
|
||||
clearLogs: (fileName?: string) => ipcRenderer.invoke('clear-logs', fileName),
|
||||
cleanOldLogs: (daysToKeep?: number) => ipcRenderer.invoke('clean-old-logs', daysToKeep),
|
||||
|
||||
// 保留原有方法以兼容现有代码
|
||||
saveLogsToFile: (logs: string) => ipcRenderer.invoke('save-logs-to-file', logs),
|
||||
loadLogsFromFile: () => ipcRenderer.invoke('load-logs-from-file'),
|
||||
|
||||
// 文件系统操作
|
||||
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
|
||||
showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', filePath),
|
||||
|
||||
// 监听下载进度
|
||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.on('download-progress', (_, progress) => callback(progress))
|
||||
},
|
||||
removeDownloadProgressListener: () => {
|
||||
ipcRenderer.removeAllListeners('download-progress')
|
||||
},
|
||||
})
|
||||
62
frontend/electron/services/downloadService.ts
Normal file
62
frontend/electron/services/downloadService.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as https from 'https'
|
||||
import * as fs from 'fs'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import * as http from 'http'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
export function setMainWindow(window: BrowserWindow) {
|
||||
mainWindow = window
|
||||
}
|
||||
|
||||
export function downloadFile(url: string, outputPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`开始下载文件: ${url}`)
|
||||
console.log(`保存路径: ${outputPath}`)
|
||||
|
||||
const file = fs.createWriteStream(outputPath)
|
||||
// 创建HTTP客户端,兼容https和http
|
||||
const client = url.startsWith('https') ? https : http
|
||||
|
||||
client
|
||||
.get(url, response => {
|
||||
const totalSize = parseInt(response.headers['content-length'] || '0', 10)
|
||||
let downloadedSize = 0
|
||||
|
||||
console.log(`文件大小: ${totalSize} bytes`)
|
||||
|
||||
response.pipe(file)
|
||||
|
||||
response.on('data', chunk => {
|
||||
downloadedSize += chunk.length
|
||||
const progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0
|
||||
|
||||
console.log(`下载进度: ${progress}% (${downloadedSize}/${totalSize})`)
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
progress,
|
||||
status: 'downloading',
|
||||
message: `下载中... ${progress}%`,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close()
|
||||
console.log(`文件下载完成: ${outputPath}`)
|
||||
resolve()
|
||||
})
|
||||
|
||||
file.on('error', err => {
|
||||
console.error(`文件写入错误: ${err.message}`)
|
||||
fs.unlink(outputPath, () => {}) // 删除不完整的文件
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
.on('error', err => {
|
||||
console.error(`下载错误: ${err.message}`)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
34
frontend/electron/services/environmentService.ts
Normal file
34
frontend/electron/services/environmentService.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { app } from 'electron'
|
||||
|
||||
// 获取应用根目录
|
||||
export function getAppRoot(): string {
|
||||
return process.env.NODE_ENV === 'development' ? process.cwd() : path.dirname(app.getPath('exe'))
|
||||
}
|
||||
|
||||
// 检查环境
|
||||
export function checkEnvironment(appRoot: string) {
|
||||
const environmentPath = path.join(appRoot, 'environment')
|
||||
const pythonPath = path.join(environmentPath, 'python')
|
||||
const gitPath = path.join(environmentPath, 'git')
|
||||
const backendPath = path.join(appRoot, 'backend')
|
||||
const requirementsPath = path.join(backendPath, 'requirements.txt')
|
||||
|
||||
const pythonExists = fs.existsSync(pythonPath)
|
||||
const gitExists = fs.existsSync(gitPath)
|
||||
const backendExists = fs.existsSync(backendPath)
|
||||
|
||||
// 检查依赖是否已安装(简单检查是否存在site-packages目录)
|
||||
const sitePackagesPath = path.join(pythonPath, 'Lib', 'site-packages')
|
||||
const dependenciesInstalled =
|
||||
fs.existsSync(sitePackagesPath) && fs.readdirSync(sitePackagesPath).length > 10
|
||||
|
||||
return {
|
||||
pythonExists,
|
||||
gitExists,
|
||||
backendExists,
|
||||
dependenciesInstalled,
|
||||
isInitialized: pythonExists && gitExists && backendExists && dependenciesInstalled,
|
||||
}
|
||||
}
|
||||
334
frontend/electron/services/gitService.ts
Normal file
334
frontend/electron/services/gitService.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { spawn } from 'child_process'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { downloadFile } from './downloadService'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
export function setMainWindow(window: BrowserWindow) {
|
||||
mainWindow = window
|
||||
}
|
||||
|
||||
const gitDownloadUrl = 'https://download.auto-mas.top/d/AUTO_MAS/git.zip'
|
||||
|
||||
// 递归复制目录,包括文件和隐藏文件
|
||||
function copyDirSync(src: string, dest: string) {
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.mkdirSync(dest, { recursive: true })
|
||||
}
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name)
|
||||
const destPath = path.join(dest, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
copyDirSync(srcPath, destPath)
|
||||
} else {
|
||||
// 直接覆盖写,不需要先删除
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Git环境变量配置
|
||||
function getGitEnvironment(appRoot: string) {
|
||||
const gitDir = path.join(appRoot, 'environment', 'git')
|
||||
const binPath = path.join(gitDir, 'bin')
|
||||
const mingw64BinPath = path.join(gitDir, 'mingw64', 'bin')
|
||||
const gitCorePath = path.join(gitDir, 'mingw64', 'libexec', 'git-core')
|
||||
|
||||
return {
|
||||
...process.env,
|
||||
// 修复remote-https问题的关键:确保所有Git相关路径都在PATH中
|
||||
PATH: `${binPath};${mingw64BinPath};${gitCorePath};${process.env.PATH}`,
|
||||
GIT_EXEC_PATH: gitCorePath,
|
||||
GIT_TEMPLATE_DIR: path.join(gitDir, 'mingw64', 'share', 'git-core', 'templates'),
|
||||
HOME: process.env.USERPROFILE || process.env.HOME,
|
||||
// // SSL证书路径
|
||||
// GIT_SSL_CAINFO: path.join(gitDir, 'mingw64', 'ssl', 'certs', 'ca-bundle.crt'),
|
||||
// 禁用系统Git配置
|
||||
GIT_CONFIG_NOSYSTEM: '1',
|
||||
// 禁用交互式认证
|
||||
GIT_TERMINAL_PROMPT: '0',
|
||||
GIT_ASKPASS: '',
|
||||
// // 修复remote-https问题的关键环境变量
|
||||
// CURL_CA_BUNDLE: path.join(gitDir, 'mingw64', 'ssl', 'certs', 'ca-bundle.crt'),
|
||||
// 确保Git能找到所有必要的程序
|
||||
GIT_HTTP_LOW_SPEED_LIMIT: '0',
|
||||
GIT_HTTP_LOW_SPEED_TIME: '0',
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为Git仓库
|
||||
function isGitRepository(dirPath: string): boolean {
|
||||
const gitDir = path.join(dirPath, '.git')
|
||||
return fs.existsSync(gitDir)
|
||||
}
|
||||
|
||||
// 下载Git
|
||||
export async function downloadGit(appRoot: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const environmentPath = path.join(appRoot, 'environment')
|
||||
const gitPath = path.join(environmentPath, 'git')
|
||||
|
||||
if (!fs.existsSync(environmentPath)) {
|
||||
fs.mkdirSync(environmentPath, { recursive: true })
|
||||
}
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'git',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: '开始下载Git...',
|
||||
})
|
||||
}
|
||||
|
||||
// 使用自定义Git压缩包
|
||||
const zipPath = path.join(environmentPath, 'git.zip')
|
||||
await downloadFile(gitDownloadUrl, zipPath)
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'git',
|
||||
progress: 100,
|
||||
status: 'extracting',
|
||||
message: '正在解压Git...',
|
||||
})
|
||||
}
|
||||
|
||||
// 解压Git到临时目录,然后移动到正确位置
|
||||
console.log(`开始解压Git到: ${gitPath}`)
|
||||
|
||||
// 创建临时解压目录
|
||||
const tempExtractPath = path.join(environmentPath, 'git_temp')
|
||||
if (!fs.existsSync(tempExtractPath)) {
|
||||
fs.mkdirSync(tempExtractPath, { recursive: true })
|
||||
console.log(`创建临时解压目录: ${tempExtractPath}`)
|
||||
}
|
||||
|
||||
// 解压到临时目录
|
||||
const zip = new AdmZip(zipPath)
|
||||
zip.extractAllTo(tempExtractPath, true)
|
||||
console.log(`Git解压到临时目录: ${tempExtractPath}`)
|
||||
|
||||
// 检查解压后的目录结构
|
||||
const tempContents = fs.readdirSync(tempExtractPath)
|
||||
console.log(`临时目录内容:`, tempContents)
|
||||
|
||||
// 如果解压后有git子目录,则从git子目录移动内容
|
||||
let sourceDir = tempExtractPath
|
||||
if (tempContents.length === 1 && tempContents[0] === 'git') {
|
||||
sourceDir = path.join(tempExtractPath, 'git')
|
||||
console.log(`检测到git子目录,使用源目录: ${sourceDir}`)
|
||||
}
|
||||
|
||||
// 确保目标Git目录存在
|
||||
if (!fs.existsSync(gitPath)) {
|
||||
fs.mkdirSync(gitPath, { recursive: true })
|
||||
console.log(`创建Git目录: ${gitPath}`)
|
||||
}
|
||||
|
||||
// 移动文件到最终目录
|
||||
const sourceContents = fs.readdirSync(sourceDir)
|
||||
for (const item of sourceContents) {
|
||||
const sourcePath = path.join(sourceDir, item)
|
||||
const targetPath = path.join(gitPath, item)
|
||||
|
||||
// 如果目标已存在,先删除
|
||||
if (fs.existsSync(targetPath)) {
|
||||
if (fs.statSync(targetPath).isDirectory()) {
|
||||
fs.rmSync(targetPath, { recursive: true, force: true })
|
||||
} else {
|
||||
fs.unlinkSync(targetPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 移动文件或目录
|
||||
fs.renameSync(sourcePath, targetPath)
|
||||
console.log(`移动: ${sourcePath} -> ${targetPath}`)
|
||||
}
|
||||
|
||||
// 清理临时目录
|
||||
fs.rmSync(tempExtractPath, { recursive: true, force: true })
|
||||
console.log(`清理临时目录: ${tempExtractPath}`)
|
||||
|
||||
console.log(`Git解压完成到: ${gitPath}`)
|
||||
|
||||
// 删除zip文件
|
||||
fs.unlinkSync(zipPath)
|
||||
console.log(`删除临时文件: ${zipPath}`)
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'git',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: 'Git安装完成',
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'git',
|
||||
progress: 0,
|
||||
status: 'error',
|
||||
message: `Git下载失败: ${errorMessage}`,
|
||||
})
|
||||
}
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
}
|
||||
|
||||
// 克隆后端代码(替换原有核心逻辑)
|
||||
export async function cloneBackend(
|
||||
appRoot: string,
|
||||
repoUrl = 'https://github.com/AUTO-MAS-Project/AUTO-MAS.git'
|
||||
): Promise<{
|
||||
success: boolean
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const backendPath = appRoot
|
||||
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
|
||||
if (!fs.existsSync(gitPath)) throw new Error(`Git可执行文件不存在: ${gitPath}`)
|
||||
const gitEnv = getGitEnvironment(appRoot)
|
||||
|
||||
// 检查 git 是否可用
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['--version'], { env: gitEnv })
|
||||
proc.on('close', code => (code === 0 ? resolve() : reject(new Error('git 无法正常运行'))))
|
||||
proc.on('error', reject)
|
||||
})
|
||||
|
||||
// ==== 下面是关键逻辑 ====
|
||||
if (isGitRepository(backendPath)) {
|
||||
// 已是 git 仓库,先更新远程URL为镜像站,然后 pull
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: '正在更新后端代码...',
|
||||
})
|
||||
}
|
||||
|
||||
// 更新远程URL为镜像站URL,避免直接访问GitHub
|
||||
console.log(`更新远程URL为镜像站: ${repoUrl}`)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['remote', 'set-url', 'origin', repoUrl], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: backendPath
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git remote set-url:', d.toString()))
|
||||
proc.stderr?.on('data', d => console.log('git remote set-url err:', d.toString()))
|
||||
proc.on('close', code =>
|
||||
code === 0 ? resolve() : reject(new Error(`git remote set-url失败,退出码: ${code}`))
|
||||
)
|
||||
proc.on('error', reject)
|
||||
})
|
||||
|
||||
// 执行pull操作
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['pull'], { stdio: 'pipe', env: gitEnv, cwd: backendPath })
|
||||
proc.stdout?.on('data', d => console.log('git pull:', d.toString()))
|
||||
proc.stderr?.on('data', d => console.log('git pull err:', d.toString()))
|
||||
proc.on('close', code =>
|
||||
code === 0 ? resolve() : reject(new Error(`git pull失败,退出码: ${code}`))
|
||||
)
|
||||
proc.on('error', reject)
|
||||
})
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: '后端代码更新完成',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 不是 git 仓库,clone 到 tmp,再拷贝出来
|
||||
const tmpDir = path.join(appRoot, 'git_tmp')
|
||||
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
fs.mkdirSync(tmpDir, { recursive: true })
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: '正在克隆后端代码...',
|
||||
})
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
gitPath,
|
||||
[
|
||||
'clone',
|
||||
'--progress',
|
||||
'--verbose',
|
||||
'--single-branch',
|
||||
'--depth',
|
||||
'1',
|
||||
'--branch',
|
||||
'feature/refactor',
|
||||
repoUrl,
|
||||
tmpDir,
|
||||
],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: appRoot,
|
||||
}
|
||||
)
|
||||
proc.stdout?.on('data', d => console.log('git clone:', d.toString()))
|
||||
proc.stderr?.on('data', d => console.log('git clone err:', d.toString()))
|
||||
proc.on('close', code =>
|
||||
code === 0 ? resolve() : reject(new Error(`git clone失败,退出码: ${code}`))
|
||||
)
|
||||
proc.on('error', reject)
|
||||
})
|
||||
|
||||
// 复制所有文件到 backendPath(appRoot),包含 .git
|
||||
const tmpFiles = fs.readdirSync(tmpDir)
|
||||
for (const file of tmpFiles) {
|
||||
const src = path.join(tmpDir, file)
|
||||
const dst = path.join(backendPath, file)
|
||||
if (fs.existsSync(dst)) {
|
||||
if (fs.statSync(dst).isDirectory()) fs.rmSync(dst, { recursive: true, force: true })
|
||||
else fs.unlinkSync(dst)
|
||||
}
|
||||
if (fs.statSync(src).isDirectory()) copyDirSync(src, dst)
|
||||
else fs.copyFileSync(src, dst)
|
||||
}
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: '后端代码克隆完成',
|
||||
})
|
||||
}
|
||||
}
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('获取后端代码失败:', errorMessage)
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'error',
|
||||
message: `后端代码获取失败: ${errorMessage}`,
|
||||
})
|
||||
}
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
}
|
||||
168
frontend/electron/services/logService.ts
Normal file
168
frontend/electron/services/logService.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import log from 'electron-log'
|
||||
import * as path from 'path'
|
||||
import { getAppRoot } from './environmentService'
|
||||
|
||||
// 移除ANSI颜色转义字符的函数
|
||||
function stripAnsiColors(text: string): string {
|
||||
// 匹配ANSI转义序列的正则表达式 - 更完整的模式
|
||||
const ansiRegex = /\x1b\[[0-9;]*[mGKHF]|\x1b\[[\d;]*[A-Za-z]/g
|
||||
return text.replace(ansiRegex, '')
|
||||
}
|
||||
|
||||
// 获取应用安装目录下的日志路径
|
||||
function getLogDirectory(): string {
|
||||
const appRoot = getAppRoot()
|
||||
return path.join(appRoot, 'logs')
|
||||
}
|
||||
|
||||
// 获取当前日期的日志文件名 - 使用ISO 8601格式
|
||||
function getTodayLogFileName(): string {
|
||||
const today = new Date()
|
||||
const year = today.getFullYear()
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(today.getDate()).padStart(2, '0')
|
||||
return `frontendlog-${year}-${month}-${day}.log`
|
||||
}
|
||||
|
||||
// 配置日志系统
|
||||
export function setupLogger() {
|
||||
// 设置日志文件路径到软件安装目录
|
||||
const logPath = getLogDirectory()
|
||||
|
||||
// 确保日志目录存在
|
||||
const fs = require('fs')
|
||||
if (!fs.existsSync(logPath)) {
|
||||
fs.mkdirSync(logPath, { recursive: true })
|
||||
}
|
||||
|
||||
// 配置日志格式
|
||||
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
|
||||
log.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
|
||||
|
||||
// 设置主进程日志文件路径和名称 - 按日期分文件
|
||||
log.transports.file.resolvePathFn = () => {
|
||||
const fileName = getTodayLogFileName()
|
||||
return path.join(logPath, fileName)
|
||||
}
|
||||
|
||||
// 设置日志级别
|
||||
log.transports.file.level = 'debug'
|
||||
log.transports.console.level = 'debug'
|
||||
|
||||
// 设置文件大小限制 (50MB,因为按日期分文件,可以设置更大)
|
||||
log.transports.file.maxSize = 50 * 1024 * 1024
|
||||
|
||||
// 禁用自动归档,因为我们按日期分文件
|
||||
log.transports.file.archiveLog = () => {
|
||||
/* do nothing */
|
||||
};
|
||||
|
||||
// 捕获未处理的异常和Promise拒绝
|
||||
log.catchErrors({
|
||||
showDialog: false,
|
||||
onError: (options: any) => {
|
||||
log.error('未处理的错误:', options.error)
|
||||
log.error('版本信息:', options.versions)
|
||||
log.error('进程类型:', options.processType)
|
||||
},
|
||||
})
|
||||
|
||||
// 重写console方法,将所有控制台输出重定向到日志
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
info: console.info,
|
||||
debug: console.debug,
|
||||
}
|
||||
|
||||
console.log = (...args) => {
|
||||
log.info(...args)
|
||||
originalConsole.log(...args)
|
||||
}
|
||||
|
||||
console.error = (...args) => {
|
||||
log.error(...args)
|
||||
originalConsole.error(...args)
|
||||
}
|
||||
|
||||
console.warn = (...args) => {
|
||||
log.warn(...args)
|
||||
originalConsole.warn(...args)
|
||||
}
|
||||
|
||||
console.info = (...args) => {
|
||||
log.info(...args)
|
||||
originalConsole.info(...args)
|
||||
}
|
||||
|
||||
console.debug = (...args) => {
|
||||
log.debug(...args)
|
||||
originalConsole.debug(...args)
|
||||
}
|
||||
|
||||
log.info('日志系统初始化完成')
|
||||
log.info(`日志文件路径: ${path.join(logPath, getTodayLogFileName())}`)
|
||||
|
||||
return log
|
||||
}
|
||||
|
||||
// 导出日志实例和工具函数
|
||||
export { log, stripAnsiColors }
|
||||
|
||||
// 获取当前日志文件路径
|
||||
export function getLogPath(): string {
|
||||
return path.join(getLogDirectory(), getTodayLogFileName())
|
||||
}
|
||||
|
||||
// 获取所有日志文件列表
|
||||
export function getLogFiles(): string[] {
|
||||
const fs = require('fs')
|
||||
const logDir = getLogDirectory()
|
||||
|
||||
if (!fs.existsSync(logDir)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(logDir)
|
||||
return files
|
||||
.filter((file: string) => file.match(/^frontendlog-\d{4}-\d{2}-\d{2}\.log$/))
|
||||
.sort()
|
||||
.reverse() // 最新的在前面
|
||||
}
|
||||
|
||||
// 清理旧日志文件
|
||||
export function cleanOldLogs(daysToKeep: number = 7) {
|
||||
const fs = require('fs')
|
||||
const logDir = getLogDirectory()
|
||||
|
||||
if (!fs.existsSync(logDir)) {
|
||||
return
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(logDir)
|
||||
const now = new Date()
|
||||
const cutoffDate = new Date(now.getTime() - daysToKeep * 24 * 60 * 60 * 1000)
|
||||
|
||||
// 格式化截止日期为YYYY-MM-DD
|
||||
const cutoffDateStr = cutoffDate.getFullYear() + '-' +
|
||||
String(cutoffDate.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(cutoffDate.getDate()).padStart(2, '0')
|
||||
|
||||
files.forEach((file: string) => {
|
||||
// 匹配日志文件名格式 frontendlog-YYYY-MM-DD.log
|
||||
const match = file.match(/^frontendlog-(\d{4}-\d{2}-\d{2})\.log$/)
|
||||
if (match) {
|
||||
const fileDateStr = match[1]
|
||||
if (fileDateStr < cutoffDateStr) {
|
||||
const filePath = path.join(logDir, file)
|
||||
try {
|
||||
fs.unlinkSync(filePath)
|
||||
log.info(`已删除旧日志文件: ${file}`)
|
||||
} catch (error) {
|
||||
log.error(`删除旧日志文件失败: ${file}`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
642
frontend/electron/services/pythonService.ts
Normal file
642
frontend/electron/services/pythonService.ts
Normal file
@@ -0,0 +1,642 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { spawn } from 'child_process'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { downloadFile } from './downloadService'
|
||||
import { ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { log, stripAnsiColors } from './logService'
|
||||
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
export function setMainWindow(window: BrowserWindow) {
|
||||
mainWindow = window
|
||||
}
|
||||
|
||||
// Python镜像源URL映射
|
||||
const pythonMirrorUrls = {
|
||||
official: 'https://www.python.org/ftp/python/3.12.0/python-3.12.0-embed-amd64.zip',
|
||||
tsinghua: 'https://mirrors.tuna.tsinghua.edu.cn/python/3.12.0/python-3.12.0-embed-amd64.zip',
|
||||
ustc: 'https://mirrors.ustc.edu.cn/python/3.12.0/python-3.12.0-embed-amd64.zip',
|
||||
huawei:
|
||||
'https://mirrors.huaweicloud.com/repository/toolkit/python/3.12.0/python-3.12.0-embed-amd64.zip',
|
||||
aliyun: 'https://mirrors.aliyun.com/python-release/windows/python-3.12.0-embed-amd64.zip',
|
||||
}
|
||||
|
||||
// 检查pip是否已安装
|
||||
function isPipInstalled(pythonPath: string): boolean {
|
||||
const scriptsPath = path.join(pythonPath, 'Scripts')
|
||||
const pipExePath = path.join(scriptsPath, 'pip.exe')
|
||||
const pip3ExePath = path.join(scriptsPath, 'pip3.exe')
|
||||
|
||||
console.log(`检查pip安装状态:`)
|
||||
console.log(`Scripts目录: ${scriptsPath}`)
|
||||
console.log(`pip.exe路径: ${pipExePath}`)
|
||||
console.log(`pip3.exe路径: ${pip3ExePath}`)
|
||||
|
||||
const scriptsExists = fs.existsSync(scriptsPath)
|
||||
const pipExists = fs.existsSync(pipExePath)
|
||||
const pip3Exists = fs.existsSync(pip3ExePath)
|
||||
|
||||
console.log(`Scripts目录存在: ${scriptsExists}`)
|
||||
console.log(`pip.exe存在: ${pipExists}`)
|
||||
console.log(`pip3.exe存在: ${pip3Exists}`)
|
||||
|
||||
return scriptsExists && (pipExists || pip3Exists)
|
||||
}
|
||||
|
||||
// 安装pip
|
||||
async function installPip(pythonPath: string, appRoot: string): Promise<void> {
|
||||
console.log('开始检查pip安装状态...')
|
||||
|
||||
const pythonExe = path.join(pythonPath, 'python.exe')
|
||||
|
||||
// 检查Python可执行文件是否存在
|
||||
if (!fs.existsSync(pythonExe)) {
|
||||
throw new Error(`Python可执行文件不存在: ${pythonExe}`)
|
||||
}
|
||||
|
||||
// 检查pip是否已安装
|
||||
if (isPipInstalled(pythonPath)) {
|
||||
console.log('pip已经安装,跳过安装步骤')
|
||||
console.log('检测到pip.exe文件存在,认为pip安装成功')
|
||||
console.log('pip检查完成')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('pip未安装,开始安装...')
|
||||
|
||||
const getPipPath = path.join(pythonPath, 'get-pip.py')
|
||||
const getPipUrl = 'https://download.auto-mas.top/d/AUTO_MAS/get-pip.py'
|
||||
|
||||
console.log(`Python可执行文件路径: ${pythonExe}`)
|
||||
console.log(`get-pip.py下载URL: ${getPipUrl}`)
|
||||
console.log(`get-pip.py保存路径: ${getPipPath}`)
|
||||
|
||||
// 下载get-pip.py
|
||||
console.log('开始下载get-pip.py...')
|
||||
try {
|
||||
await downloadFile(getPipUrl, getPipPath)
|
||||
console.log('get-pip.py下载完成')
|
||||
|
||||
// 检查下载的文件大小
|
||||
const stats = fs.statSync(getPipPath)
|
||||
console.log(`get-pip.py文件大小: ${stats.size} bytes`)
|
||||
|
||||
if (stats.size < 10000) {
|
||||
// 如果文件小于10KB,可能是无效文件
|
||||
throw new Error(`get-pip.py文件大小异常: ${stats.size} bytes,可能下载失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载get-pip.py失败:', error)
|
||||
throw new Error(`下载get-pip.py失败: ${error}`)
|
||||
}
|
||||
|
||||
// 执行pip安装
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
console.log('执行pip安装命令...')
|
||||
|
||||
const process = spawn(pythonExe, [getPipPath], {
|
||||
cwd: pythonPath,
|
||||
stdio: 'pipe',
|
||||
})
|
||||
|
||||
process.stdout?.on('data', data => {
|
||||
const output = stripAnsiColors(data.toString())
|
||||
log.info('pip安装输出:', output)
|
||||
})
|
||||
|
||||
process.stderr?.on('data', data => {
|
||||
const errorOutput = stripAnsiColors(data.toString())
|
||||
log.warn('pip安装错误输出:', errorOutput)
|
||||
})
|
||||
|
||||
process.on('close', code => {
|
||||
console.log(`pip安装完成,退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log('pip安装成功')
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`pip安装失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
|
||||
process.on('error', error => {
|
||||
console.error('pip安装进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// 验证pip是否安装成功
|
||||
console.log('验证pip安装...')
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const verifyProcess = spawn(pythonExe, ['-m', 'pip', '--version'], {
|
||||
cwd: pythonPath,
|
||||
stdio: 'pipe',
|
||||
})
|
||||
|
||||
verifyProcess.stdout?.on('data', data => {
|
||||
const output = stripAnsiColors(data.toString())
|
||||
log.info('pip版本信息:', output)
|
||||
})
|
||||
|
||||
verifyProcess.stderr?.on('data', data => {
|
||||
const errorOutput = stripAnsiColors(data.toString())
|
||||
log.warn('pip版本检查错误:', errorOutput)
|
||||
})
|
||||
|
||||
verifyProcess.on('close', code => {
|
||||
if (code === 0) {
|
||||
console.log('pip验证成功')
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`pip验证失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
|
||||
verifyProcess.on('error', error => {
|
||||
console.error('pip验证进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// 清理临时文件
|
||||
console.log('清理临时文件...')
|
||||
try {
|
||||
if (fs.existsSync(getPipPath)) {
|
||||
fs.unlinkSync(getPipPath)
|
||||
console.log('get-pip.py临时文件已删除')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('清理get-pip.py文件时出错:', error)
|
||||
}
|
||||
|
||||
console.log('pip安装和验证完成')
|
||||
}
|
||||
|
||||
// 下载Python
|
||||
export async function downloadPython(
|
||||
appRoot: string,
|
||||
mirror = 'ustc'
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const environmentPath = path.join(appRoot, 'environment')
|
||||
const pythonPath = path.join(environmentPath, 'python')
|
||||
|
||||
// 确保environment目录存在
|
||||
if (!fs.existsSync(environmentPath)) {
|
||||
fs.mkdirSync(environmentPath, { recursive: true })
|
||||
}
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'python',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: '开始下载Python...',
|
||||
})
|
||||
}
|
||||
|
||||
// 根据选择的镜像源获取下载链接
|
||||
const pythonUrl =
|
||||
pythonMirrorUrls[mirror as keyof typeof pythonMirrorUrls] || pythonMirrorUrls.ustc
|
||||
const zipPath = path.join(environmentPath, 'python.zip')
|
||||
|
||||
await downloadFile(pythonUrl, zipPath)
|
||||
|
||||
// 检查下载的Python文件大小
|
||||
const stats = fs.statSync(zipPath)
|
||||
console.log(
|
||||
`Python压缩包大小: ${stats.size} bytes (${(stats.size / 1024 / 1024).toFixed(2)} MB)`
|
||||
)
|
||||
|
||||
// Python 3.12.0嵌入式版本应该大约30MB,如果小于5MB可能是无效文件
|
||||
if (stats.size < 5 * 1024 * 1024) {
|
||||
// 5MB
|
||||
fs.unlinkSync(zipPath) // 删除无效文件
|
||||
throw new Error(
|
||||
`Python下载文件大小异常: ${stats.size} bytes (${(stats.size / 1024).toFixed(2)} KB)。可能是对应镜像站不可用。请选择任意一个其他镜像源进行下载!`
|
||||
)
|
||||
}
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'python',
|
||||
progress: 100,
|
||||
status: 'extracting',
|
||||
message: '正在解压Python...',
|
||||
})
|
||||
}
|
||||
|
||||
// 解压Python到指定目录
|
||||
console.log(`开始解压Python到: ${pythonPath}`)
|
||||
|
||||
// 确保Python目录存在
|
||||
if (!fs.existsSync(pythonPath)) {
|
||||
fs.mkdirSync(pythonPath, { recursive: true })
|
||||
console.log(`创建Python目录: ${pythonPath}`)
|
||||
}
|
||||
|
||||
const zip = new AdmZip(zipPath)
|
||||
zip.extractAllTo(pythonPath, true)
|
||||
console.log(`Python解压完成到: ${pythonPath}`)
|
||||
|
||||
// 删除zip文件
|
||||
fs.unlinkSync(zipPath)
|
||||
console.log(`删除临时文件: ${zipPath}`)
|
||||
|
||||
// 启用 site-packages 支持
|
||||
const pthFile = path.join(pythonPath, 'python312._pth')
|
||||
if (fs.existsSync(pthFile)) {
|
||||
let content = fs.readFileSync(pthFile, 'utf-8')
|
||||
content = content.replace(/^#import site/m, 'import site')
|
||||
fs.writeFileSync(pthFile, content, 'utf-8')
|
||||
console.log('已启用 site-packages 支持')
|
||||
}
|
||||
|
||||
// 安装pip
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'python',
|
||||
progress: 80,
|
||||
status: 'installing',
|
||||
message: '正在安装pip...',
|
||||
})
|
||||
}
|
||||
|
||||
await installPip(pythonPath, appRoot)
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'python',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: 'Python和pip安装完成',
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'python',
|
||||
progress: 0,
|
||||
status: 'error',
|
||||
message: `Python下载失败: ${errorMessage}`,
|
||||
})
|
||||
}
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
}
|
||||
|
||||
// pip镜像源URL映射
|
||||
const pipMirrorUrls = {
|
||||
official: 'https://pypi.org/simple/',
|
||||
tsinghua: 'https://pypi.tuna.tsinghua.edu.cn/simple/',
|
||||
ustc: 'https://pypi.mirrors.ustc.edu.cn/simple/',
|
||||
aliyun: 'https://mirrors.aliyun.com/pypi/simple/',
|
||||
douban: 'https://pypi.douban.com/simple/',
|
||||
}
|
||||
|
||||
// 安装Python依赖
|
||||
export async function installDependencies(
|
||||
appRoot: string,
|
||||
mirror = 'tsinghua'
|
||||
): Promise<{
|
||||
success: boolean
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const pythonPath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
||||
const backendPath = path.join(appRoot)
|
||||
const requirementsPath = path.join(appRoot, 'requirements.txt')
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(pythonPath)) {
|
||||
throw new Error('Python可执行文件不存在')
|
||||
}
|
||||
if (!fs.existsSync(requirementsPath)) {
|
||||
throw new Error('requirements.txt文件不存在')
|
||||
}
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'dependencies',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: '正在安装Python依赖包...',
|
||||
})
|
||||
}
|
||||
|
||||
// 获取pip镜像源URL
|
||||
const pipMirrorUrl =
|
||||
pipMirrorUrls[mirror as keyof typeof pipMirrorUrls] || pipMirrorUrls.tsinghua
|
||||
|
||||
// 使用Scripts文件夹中的pip.exe
|
||||
const pythonDir = path.join(appRoot, 'environment', 'python')
|
||||
const pipExePath = path.join(pythonDir, 'Scripts', 'pip.exe')
|
||||
|
||||
console.log(`开始安装Python依赖`)
|
||||
console.log(`Python目录: ${pythonDir}`)
|
||||
console.log(`pip.exe路径: ${pipExePath}`)
|
||||
console.log(`requirements.txt路径: ${requirementsPath}`)
|
||||
console.log(`pip镜像源: ${pipMirrorUrl}`)
|
||||
|
||||
// 检查pip.exe是否存在
|
||||
if (!fs.existsSync(pipExePath)) {
|
||||
throw new Error(`pip.exe不存在: ${pipExePath}`)
|
||||
}
|
||||
|
||||
// 安装依赖 - 直接使用pip.exe而不是python -m pip
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const process = spawn(
|
||||
pipExePath,
|
||||
[
|
||||
'install',
|
||||
'-r',
|
||||
requirementsPath,
|
||||
'-i',
|
||||
pipMirrorUrl,
|
||||
'--trusted-host',
|
||||
new URL(pipMirrorUrl).hostname,
|
||||
],
|
||||
{
|
||||
cwd: backendPath,
|
||||
stdio: 'pipe',
|
||||
}
|
||||
)
|
||||
|
||||
process.stdout?.on('data', data => {
|
||||
const output = stripAnsiColors(data.toString())
|
||||
log.info('Pip output:', output)
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'dependencies',
|
||||
progress: 50,
|
||||
status: 'downloading',
|
||||
message: '正在安装依赖包...',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
process.stderr?.on('data', data => {
|
||||
const errorOutput = stripAnsiColors(data.toString())
|
||||
log.error('Pip error:', errorOutput)
|
||||
})
|
||||
|
||||
process.on('close', code => {
|
||||
console.log(`pip安装完成,退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`依赖安装失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
|
||||
process.on('error', error => {
|
||||
console.error('pip进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'dependencies',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: 'Python依赖安装完成',
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'dependencies',
|
||||
progress: 0,
|
||||
status: 'error',
|
||||
message: `依赖安装失败: ${errorMessage}`,
|
||||
})
|
||||
}
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
}
|
||||
|
||||
// 导出pip安装函数
|
||||
export async function installPipPackage(
|
||||
appRoot: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const pythonPath = path.join(appRoot, 'environment', 'python')
|
||||
|
||||
if (!fs.existsSync(pythonPath)) {
|
||||
throw new Error('Python环境不存在,请先安装Python')
|
||||
}
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'pip',
|
||||
progress: 0,
|
||||
status: 'installing',
|
||||
message: '正在安装pip...',
|
||||
})
|
||||
}
|
||||
|
||||
await installPip(pythonPath, appRoot)
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'pip',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: 'pip安装完成',
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'pip',
|
||||
progress: 0,
|
||||
status: 'error',
|
||||
message: `pip安装失败: ${errorMessage}`,
|
||||
})
|
||||
}
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
}
|
||||
|
||||
// 启动后端
|
||||
let backendProc: ChildProcessWithoutNullStreams | null = null
|
||||
|
||||
/**
|
||||
* 启动后端
|
||||
* @param appRoot 项目根目录
|
||||
* @param timeoutMs 等待启动超时(默认 30 秒)
|
||||
*/
|
||||
export async function startBackend(appRoot: string, timeoutMs = 30_000) {
|
||||
try {
|
||||
// 如果已经在运行,直接返回
|
||||
if (backendProc && !backendProc.killed && backendProc.exitCode == null) {
|
||||
console.log('[Backend] 已在运行, PID =', backendProc.pid)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const pythonExe = path.join(appRoot, 'environment', 'python', 'python.exe')
|
||||
const mainPy = path.join(appRoot, 'main.py')
|
||||
|
||||
if (!fs.existsSync(pythonExe)) {
|
||||
throw new Error(`Python可执行文件不存在: ${pythonExe}`)
|
||||
}
|
||||
if (!fs.existsSync(mainPy)) {
|
||||
throw new Error(`后端主文件不存在: ${mainPy}`)
|
||||
}
|
||||
|
||||
console.log(`[Backend] spawn "${pythonExe}" "${mainPy}" (cwd=${appRoot})`)
|
||||
|
||||
backendProc = spawn(pythonExe, [mainPy], {
|
||||
cwd: appRoot,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
||||
})
|
||||
|
||||
backendProc.stdout.setEncoding('utf8')
|
||||
backendProc.stderr.setEncoding('utf8')
|
||||
|
||||
backendProc.stdout.on('data', d => {
|
||||
const line = stripAnsiColors(d.toString().trim())
|
||||
if (line) log.info('[Backend]', line)
|
||||
})
|
||||
backendProc.stderr.on('data', d => {
|
||||
const line = stripAnsiColors(d.toString().trim())
|
||||
if (line) log.info('[Backend]', line)
|
||||
})
|
||||
|
||||
backendProc.once('exit', (code, signal) => {
|
||||
console.log('[Backend] 退出', { code, signal })
|
||||
backendProc = null
|
||||
})
|
||||
backendProc.once('error', e => {
|
||||
console.error('[Backend] 进程错误:', e)
|
||||
})
|
||||
|
||||
// 等待启动成功(匹配 Uvicorn 的输出)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let settled = false
|
||||
const timer = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
reject(new Error('后端启动超时'))
|
||||
}
|
||||
}, timeoutMs)
|
||||
|
||||
const checkReady = (buf: Buffer | string) => {
|
||||
if (settled) return
|
||||
const s = buf.toString()
|
||||
if (/Uvicorn running|http:\/\/0\.0\.0\.0:\d+/.test(s)) {
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
backendProc!.stdout.on('data', checkReady)
|
||||
backendProc!.stderr.on('data', checkReady)
|
||||
|
||||
backendProc!.once('exit', (code, sig) => {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
reject(new Error(`后端提前退出: code=${code}, signal=${sig ?? ''}`))
|
||||
}
|
||||
})
|
||||
backendProc!.once('error', err => {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log('[Backend] 启动成功, PID =', backendProc.pid)
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
console.error('[Backend] 启动失败:', e)
|
||||
return { success: false, error: e instanceof Error ? e.message : String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止后端进程(如果没启动就直接返回成功) */
|
||||
export async function stopBackend() {
|
||||
if (!backendProc || backendProc.killed) {
|
||||
console.log('[Backend] 未运行,无需停止')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const pid = backendProc.pid
|
||||
console.log('[Backend] 正在停止后端服务, PID =', pid)
|
||||
|
||||
return new Promise<{ success: boolean; error?: string }>(resolve => {
|
||||
// 设置超时,确保不会无限等待
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn('[Backend] 停止超时,强制结束进程')
|
||||
try {
|
||||
if (backendProc && !backendProc.killed) {
|
||||
// 在 Windows 上使用 taskkill 强制结束进程树
|
||||
if (process.platform === 'win32') {
|
||||
const { exec } = require('child_process')
|
||||
exec(`taskkill /f /t /pid ${pid}`, (error: any) => {
|
||||
if (error) {
|
||||
console.error('[Backend] taskkill 失败:', error)
|
||||
} else {
|
||||
console.log('[Backend] 进程树已强制结束')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
backendProc.kill('SIGKILL')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Backend] 强制结束失败:', e)
|
||||
}
|
||||
backendProc = null
|
||||
resolve({ success: true })
|
||||
}, 2000) // 2秒超时
|
||||
|
||||
// 清监听,避免重复日志
|
||||
backendProc?.stdout?.removeAllListeners('data')
|
||||
backendProc?.stderr?.removeAllListeners('data')
|
||||
|
||||
backendProc!.once('exit', (code, signal) => {
|
||||
clearTimeout(timeout)
|
||||
console.log('[Backend] 已退出', { code, signal })
|
||||
backendProc = null
|
||||
resolve({ success: true })
|
||||
})
|
||||
|
||||
backendProc!.once('error', err => {
|
||||
clearTimeout(timeout)
|
||||
console.error('[Backend] 停止时出错:', err)
|
||||
backendProc = null
|
||||
resolve({ success: false, error: err instanceof Error ? err.message : String(err) })
|
||||
})
|
||||
|
||||
try {
|
||||
// 首先尝试优雅关闭
|
||||
backendProc!.kill('SIGTERM')
|
||||
console.log('[Backend] 已发送 SIGTERM 信号')
|
||||
} catch (e) {
|
||||
clearTimeout(timeout)
|
||||
console.error('[Backend] kill 调用失败:', e)
|
||||
backendProc = null
|
||||
resolve({ success: false, error: e instanceof Error ? e.message : String(e) })
|
||||
}
|
||||
})
|
||||
}
|
||||
125
frontend/electron/utils/processManager.ts
Normal file
125
frontend/electron/utils/processManager.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { exec } from 'child_process'
|
||||
import * as path from 'path'
|
||||
import { getAppRoot } from '../services/environmentService'
|
||||
|
||||
export interface ProcessInfo {
|
||||
pid: number
|
||||
name: string
|
||||
commandLine: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有相关的进程信息
|
||||
*/
|
||||
export async function getRelatedProcesses(): Promise<ProcessInfo[]> {
|
||||
return new Promise((resolve) => {
|
||||
if (process.platform !== 'win32') {
|
||||
resolve([])
|
||||
return
|
||||
}
|
||||
|
||||
const appRoot = getAppRoot()
|
||||
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
||||
|
||||
// 使用 wmic 获取详细的进程信息
|
||||
const cmd = `wmic process where "Name='python.exe' or Name='AUTO-MAS.exe' or CommandLine like '%main.py%'" get ProcessId,Name,CommandLine /format:csv`
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('获取进程信息失败:', error)
|
||||
resolve([])
|
||||
return
|
||||
}
|
||||
|
||||
const processes: ProcessInfo[] = []
|
||||
const lines = stdout.split('\n').filter(line => line.trim() && !line.startsWith('Node'))
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(',')
|
||||
if (parts.length >= 4) {
|
||||
const commandLine = parts[1] || ''
|
||||
const name = parts[2] || ''
|
||||
const pid = parseInt(parts[3]) || 0
|
||||
|
||||
if (pid > 0 && (
|
||||
commandLine.includes(pythonExePath) ||
|
||||
commandLine.includes('main.py') ||
|
||||
name === 'AUTO-MAS.exe'
|
||||
)) {
|
||||
processes.push({ pid, name, commandLine })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve(processes)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制结束指定的进程
|
||||
*/
|
||||
export async function killProcess(pid: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
if (process.platform !== 'win32') {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
exec(`taskkill /f /t /pid ${pid}`, (error) => {
|
||||
if (error) {
|
||||
console.error(`结束进程 ${pid} 失败:`, error.message)
|
||||
resolve(false)
|
||||
} else {
|
||||
console.log(`进程 ${pid} 已结束`)
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制结束所有相关进程
|
||||
*/
|
||||
export async function killAllRelatedProcesses(): Promise<void> {
|
||||
console.log('开始清理所有相关进程...')
|
||||
|
||||
const processes = await getRelatedProcesses()
|
||||
console.log(`找到 ${processes.length} 个相关进程:`)
|
||||
|
||||
for (const proc of processes) {
|
||||
console.log(`- PID: ${proc.pid}, Name: ${proc.name}, CMD: ${proc.commandLine.substring(0, 100)}...`)
|
||||
}
|
||||
|
||||
// 并行结束所有进程
|
||||
const killPromises = processes.map(proc => killProcess(proc.pid))
|
||||
await Promise.all(killPromises)
|
||||
|
||||
console.log('进程清理完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待进程结束
|
||||
*/
|
||||
export async function waitForProcessExit(pid: number, timeoutMs: number = 5000): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const startTime = Date.now()
|
||||
|
||||
const checkProcess = () => {
|
||||
if (Date.now() - startTime > timeoutMs) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
exec(`tasklist /fi "PID eq ${pid}"`, (error, stdout) => {
|
||||
if (error || !stdout.includes(pid.toString())) {
|
||||
resolve(true)
|
||||
} else {
|
||||
setTimeout(checkProcess, 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
checkProcess()
|
||||
})
|
||||
}
|
||||
34
frontend/eslint.config.js
Normal file
34
frontend/eslint.config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const vue = require('eslint-plugin-vue');
|
||||
const ts = require('@typescript-eslint/eslint-plugin');
|
||||
const tsParser = require('@typescript-eslint/parser');
|
||||
const prettier = require('eslint-plugin-prettier');
|
||||
|
||||
module.exports = [
|
||||
// 推荐的 vue3 配置
|
||||
vue.configs['vue3-recommended'],
|
||||
// 推荐的 ts 配置
|
||||
ts.configs.recommended,
|
||||
// 推荐的 prettier 配置
|
||||
prettier.configs.recommended,
|
||||
// 自定义规则和文件范围
|
||||
{
|
||||
files: ['**/*.js', '**/*.ts', '**/*.vue'],
|
||||
ignores: ['dist/**', 'node_modules/**'],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2021,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: {
|
||||
vue,
|
||||
'@typescript-eslint': ts,
|
||||
prettier,
|
||||
},
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
// 如果你希望 prettier 报错,取消注释下面一行
|
||||
// 'prettier/prettier': 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AUTO-MAS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
116
frontend/package.json
Normal file
116
frontend/package.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "5.0.0-alpha.2",
|
||||
"main": "dist-electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"",
|
||||
"watch:main": "tsc -p tsconfig.electron.json --watch",
|
||||
"electron-dev": "wait-on http://localhost:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .",
|
||||
"build:main": "tsc -p tsconfig.electron.json",
|
||||
"build": "vite build && yarn build:main && electron-builder",
|
||||
"web": "vite",
|
||||
"release": "vite build && yarn build:main && electron-builder --win --publish always"
|
||||
},
|
||||
"build": {
|
||||
"asar": true,
|
||||
"asarUnpack": [],
|
||||
"extraMetadata": {
|
||||
"env": "prod"
|
||||
},
|
||||
"appId": "top.auto-mas.frontend",
|
||||
"productName": "AUTO-MAS",
|
||||
"files": [
|
||||
"dist/**",
|
||||
"dist-electron/**",
|
||||
"public/**",
|
||||
"!src/**",
|
||||
"!**/*.map"
|
||||
],
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "AUTO-MAS-Project",
|
||||
"repo": "AUTO-MAS"
|
||||
}
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "src/assets",
|
||||
"to": "assets",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"requestedExecutionLevel": "requireAdministrator",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "public/AUTO-MAS.ico",
|
||||
"artifactName": "AUTO-MAS-Setup-${version}-${arch}.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": true,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"createDesktopShortcut": true,
|
||||
"shortcutName": "AUTO-MAS",
|
||||
"differentialPackage": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"ant-design-vue": "4.x",
|
||||
"axios": "^1.11.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"electron-log": "^5.4.3",
|
||||
"form-data": "^4.0.4",
|
||||
"markdown-it": "^14.1.0",
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "4",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.17.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"concurrently": "^9.2.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"electron": "^37.2.5",
|
||||
"electron-builder": "^26.0.12",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"openapi-typescript-codegen": "^0.29.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.4",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^2.2.12",
|
||||
"wait-on": "^8.0.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "22.17.1"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
}
|
||||
BIN
frontend/public/AUTO-MAS.ico
Normal file
BIN
frontend/public/AUTO-MAS.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 450 KiB |
98
frontend/src/App.vue
Normal file
98
frontend/src/App.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ConfigProvider } from 'ant-design-vue'
|
||||
import { useTheme } from './composables/useTheme.ts'
|
||||
import { useUpdateModal } from './composables/useUpdateChecker.ts'
|
||||
import AppLayout from './components/AppLayout.vue'
|
||||
import TitleBar from './components/TitleBar.vue'
|
||||
import UpdateModal from './components/UpdateModal.vue'
|
||||
import DevDebugPanel from './components/DevDebugPanel.vue'
|
||||
import GlobalPowerCountdown from './components/GlobalPowerCountdown.vue'
|
||||
import WebSocketMessageListener from './components/WebSocketMessageListener.vue'
|
||||
import WebSocketDebugPanel from './components/WebSocketDebugPanel.vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
const route = useRoute()
|
||||
const { antdTheme, initTheme } = useTheme()
|
||||
const { updateVisible, updateData, onUpdateConfirmed } = useUpdateModal()
|
||||
|
||||
// 判断是否为初始化页面
|
||||
const isInitializationPage = computed(() => route.name === 'Initialization')
|
||||
|
||||
onMounted(() => {
|
||||
logger.info('App组件已挂载')
|
||||
initTheme()
|
||||
logger.info('主题初始化完成')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :theme="antdTheme" :locale="zhCN">
|
||||
<!-- 初始化页面使用带标题栏的全屏布局 -->
|
||||
<div v-if="isInitializationPage" class="initialization-container">
|
||||
<TitleBar />
|
||||
<div class="initialization-content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 其他页面使用带标题栏的应用布局 -->
|
||||
<div v-else class="app-container">
|
||||
<TitleBar />
|
||||
<AppLayout />
|
||||
</div>
|
||||
|
||||
<!-- 全局更新模态框 -->
|
||||
<UpdateModal
|
||||
v-model:visible="updateVisible"
|
||||
:update-data="updateData"
|
||||
@confirmed="onUpdateConfirmed"
|
||||
/>
|
||||
|
||||
<!-- 开发环境调试面板 -->
|
||||
<DevDebugPanel />
|
||||
|
||||
<!-- 全局电源倒计时弹窗 -->
|
||||
<GlobalPowerCountdown />
|
||||
|
||||
<!-- WebSocket 消息监听组件 -->
|
||||
<WebSocketMessageListener />
|
||||
|
||||
<!-- WebSocket 调试面板 (仅开发环境) -->
|
||||
<WebSocketDebugPanel v-if="$route.query.debug === 'true'" />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.initialization-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.initialization-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
/* 隐藏 Webkit 浏览器的滚动条 */
|
||||
.initialization-content::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
25
frontend/src/api/core/ApiError.ts
Normal file
25
frontend/src/api/core/ApiError.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly url: string;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly body: any;
|
||||
public readonly request: ApiRequestOptions;
|
||||
|
||||
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
||||
super(message);
|
||||
|
||||
this.name = 'ApiError';
|
||||
this.url = response.url;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.body = response.body;
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
17
frontend/src/api/core/ApiRequestOptions.ts
Normal file
17
frontend/src/api/core/ApiRequestOptions.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiRequestOptions = {
|
||||
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
|
||||
readonly url: string;
|
||||
readonly path?: Record<string, any>;
|
||||
readonly cookies?: Record<string, any>;
|
||||
readonly headers?: Record<string, any>;
|
||||
readonly query?: Record<string, any>;
|
||||
readonly formData?: Record<string, any>;
|
||||
readonly body?: any;
|
||||
readonly mediaType?: string;
|
||||
readonly responseHeader?: string;
|
||||
readonly errors?: Record<number, string>;
|
||||
};
|
||||
11
frontend/src/api/core/ApiResult.ts
Normal file
11
frontend/src/api/core/ApiResult.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiResult = {
|
||||
readonly url: string;
|
||||
readonly ok: boolean;
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly body: any;
|
||||
};
|
||||
131
frontend/src/api/core/CancelablePromise.ts
Normal file
131
frontend/src/api/core/CancelablePromise.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export class CancelError extends Error {
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'CancelError';
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OnCancel {
|
||||
readonly isResolved: boolean;
|
||||
readonly isRejected: boolean;
|
||||
readonly isCancelled: boolean;
|
||||
|
||||
(cancelHandler: () => void): void;
|
||||
}
|
||||
|
||||
export class CancelablePromise<T> implements Promise<T> {
|
||||
#isResolved: boolean;
|
||||
#isRejected: boolean;
|
||||
#isCancelled: boolean;
|
||||
readonly #cancelHandlers: (() => void)[];
|
||||
readonly #promise: Promise<T>;
|
||||
#resolve?: (value: T | PromiseLike<T>) => void;
|
||||
#reject?: (reason?: any) => void;
|
||||
|
||||
constructor(
|
||||
executor: (
|
||||
resolve: (value: T | PromiseLike<T>) => void,
|
||||
reject: (reason?: any) => void,
|
||||
onCancel: OnCancel
|
||||
) => void
|
||||
) {
|
||||
this.#isResolved = false;
|
||||
this.#isRejected = false;
|
||||
this.#isCancelled = false;
|
||||
this.#cancelHandlers = [];
|
||||
this.#promise = new Promise<T>((resolve, reject) => {
|
||||
this.#resolve = resolve;
|
||||
this.#reject = reject;
|
||||
|
||||
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isResolved = true;
|
||||
if (this.#resolve) this.#resolve(value);
|
||||
};
|
||||
|
||||
const onReject = (reason?: any): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isRejected = true;
|
||||
if (this.#reject) this.#reject(reason);
|
||||
};
|
||||
|
||||
const onCancel = (cancelHandler: () => void): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#cancelHandlers.push(cancelHandler);
|
||||
};
|
||||
|
||||
Object.defineProperty(onCancel, 'isResolved', {
|
||||
get: (): boolean => this.#isResolved,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isRejected', {
|
||||
get: (): boolean => this.#isRejected,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isCancelled', {
|
||||
get: (): boolean => this.#isCancelled,
|
||||
});
|
||||
|
||||
return executor(onResolve, onReject, onCancel as OnCancel);
|
||||
});
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return "Cancellable Promise";
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.#promise.then(onFulfilled, onRejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
|
||||
): Promise<T | TResult> {
|
||||
return this.#promise.catch(onRejected);
|
||||
}
|
||||
|
||||
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||
return this.#promise.finally(onFinally);
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isCancelled = true;
|
||||
if (this.#cancelHandlers.length) {
|
||||
try {
|
||||
for (const cancelHandler of this.#cancelHandlers) {
|
||||
cancelHandler();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Cancellation threw an error', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.#cancelHandlers.length = 0;
|
||||
if (this.#reject) this.#reject(new CancelError('Request aborted'));
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return this.#isCancelled;
|
||||
}
|
||||
}
|
||||
32
frontend/src/api/core/OpenAPI.ts
Normal file
32
frontend/src/api/core/OpenAPI.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
||||
TOKEN?: string | Resolver<string> | undefined;
|
||||
USERNAME?: string | Resolver<string> | undefined;
|
||||
PASSWORD?: string | Resolver<string> | undefined;
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: '',
|
||||
VERSION: '1.0.0',
|
||||
WITH_CREDENTIALS: false,
|
||||
CREDENTIALS: 'include',
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
ENCODE_PATH: undefined,
|
||||
};
|
||||
323
frontend/src/api/core/request.ts
Normal file
323
frontend/src/api/core/request.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import axios from 'axios';
|
||||
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
|
||||
import FormData from 'form-data';
|
||||
|
||||
import { ApiError } from './ApiError';
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
import { CancelablePromise } from './CancelablePromise';
|
||||
import type { OnCancel } from './CancelablePromise';
|
||||
import type { OpenAPIConfig } from './OpenAPI';
|
||||
|
||||
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
|
||||
return value !== undefined && value !== null;
|
||||
};
|
||||
|
||||
export const isString = (value: any): value is string => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
export const isStringWithValue = (value: any): value is string => {
|
||||
return isString(value) && value !== '';
|
||||
};
|
||||
|
||||
export const isBlob = (value: any): value is Blob => {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
typeof value.type === 'string' &&
|
||||
typeof value.stream === 'function' &&
|
||||
typeof value.arrayBuffer === 'function' &&
|
||||
typeof value.constructor === 'function' &&
|
||||
typeof value.constructor.name === 'string' &&
|
||||
/^(Blob|File)$/.test(value.constructor.name) &&
|
||||
/^(Blob|File)$/.test(value[Symbol.toStringTag])
|
||||
);
|
||||
};
|
||||
|
||||
export const isFormData = (value: any): value is FormData => {
|
||||
return value instanceof FormData;
|
||||
};
|
||||
|
||||
export const isSuccess = (status: number): boolean => {
|
||||
return status >= 200 && status < 300;
|
||||
};
|
||||
|
||||
export const base64 = (str: string): string => {
|
||||
try {
|
||||
return btoa(str);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
return Buffer.from(str).toString('base64');
|
||||
}
|
||||
};
|
||||
|
||||
export const getQueryString = (params: Record<string, any>): string => {
|
||||
const qs: string[] = [];
|
||||
|
||||
const append = (key: string, value: any) => {
|
||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
};
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isDefined(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
process(key, v);
|
||||
});
|
||||
} else if (typeof value === 'object') {
|
||||
Object.entries(value).forEach(([k, v]) => {
|
||||
process(`${key}[${k}]`, v);
|
||||
});
|
||||
} else {
|
||||
append(key, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
process(key, value);
|
||||
});
|
||||
|
||||
if (qs.length > 0) {
|
||||
return `?${qs.join('&')}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||
const encoder = config.ENCODE_PATH || encodeURI;
|
||||
|
||||
const path = options.url
|
||||
.replace('{api-version}', config.VERSION)
|
||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||
if (options.path?.hasOwnProperty(group)) {
|
||||
return encoder(String(options.path[group]));
|
||||
}
|
||||
return substring;
|
||||
});
|
||||
|
||||
const url = `${config.BASE}${path}`;
|
||||
if (options.query) {
|
||||
return `${url}${getQueryString(options.query)}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
|
||||
if (options.formData) {
|
||||
const formData = new FormData();
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isString(value) || isBlob(value)) {
|
||||
formData.append(key, value);
|
||||
} else {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(options.formData)
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => process(key, v));
|
||||
} else {
|
||||
process(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
|
||||
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
|
||||
if (typeof resolver === 'function') {
|
||||
return (resolver as Resolver<T>)(options);
|
||||
}
|
||||
return resolver;
|
||||
};
|
||||
|
||||
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => {
|
||||
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||
resolve(options, config.TOKEN),
|
||||
resolve(options, config.USERNAME),
|
||||
resolve(options, config.PASSWORD),
|
||||
resolve(options, config.HEADERS),
|
||||
]);
|
||||
|
||||
const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {}
|
||||
|
||||
const headers = Object.entries({
|
||||
Accept: 'application/json',
|
||||
...additionalHeaders,
|
||||
...options.headers,
|
||||
...formHeaders,
|
||||
})
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.reduce((headers, [key, value]) => ({
|
||||
...headers,
|
||||
[key]: String(value),
|
||||
}), {} as Record<string, string>);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = base64(`${username}:${password}`);
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
if (options.body !== undefined) {
|
||||
if (options.mediaType) {
|
||||
headers['Content-Type'] = options.mediaType;
|
||||
} else if (isBlob(options.body)) {
|
||||
headers['Content-Type'] = options.body.type || 'application/octet-stream';
|
||||
} else if (isString(options.body)) {
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
} else if (!isFormData(options.body)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const getRequestBody = (options: ApiRequestOptions): any => {
|
||||
if (options.body) {
|
||||
return options.body;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const sendRequest = async <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions,
|
||||
url: string,
|
||||
body: any,
|
||||
formData: FormData | undefined,
|
||||
headers: Record<string, string>,
|
||||
onCancel: OnCancel,
|
||||
axiosClient: AxiosInstance
|
||||
): Promise<AxiosResponse<T>> => {
|
||||
const source = axios.CancelToken.source();
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
url,
|
||||
headers,
|
||||
data: body ?? formData,
|
||||
method: options.method,
|
||||
withCredentials: config.WITH_CREDENTIALS,
|
||||
withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false,
|
||||
cancelToken: source.token,
|
||||
};
|
||||
|
||||
onCancel(() => source.cancel('The user aborted a request.'));
|
||||
|
||||
try {
|
||||
return await axiosClient.request(requestConfig);
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<T>;
|
||||
if (axiosError.response) {
|
||||
return axiosError.response;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getResponseHeader = (response: AxiosResponse<any>, responseHeader?: string): string | undefined => {
|
||||
if (responseHeader) {
|
||||
const content = response.headers[responseHeader];
|
||||
if (isString(content)) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getResponseBody = (response: AxiosResponse<any>): any => {
|
||||
if (response.status !== 204) {
|
||||
return response.data;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
|
||||
const errors: Record<number, string> = {
|
||||
400: 'Bad Request',
|
||||
401: 'Unauthorized',
|
||||
403: 'Forbidden',
|
||||
404: 'Not Found',
|
||||
500: 'Internal Server Error',
|
||||
502: 'Bad Gateway',
|
||||
503: 'Service Unavailable',
|
||||
...options.errors,
|
||||
}
|
||||
|
||||
const error = errors[result.status];
|
||||
if (error) {
|
||||
throw new ApiError(options, result, error);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
const errorStatus = result.status ?? 'unknown';
|
||||
const errorStatusText = result.statusText ?? 'unknown';
|
||||
const errorBody = (() => {
|
||||
try {
|
||||
return JSON.stringify(result.body, null, 2);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
throw new ApiError(options, result,
|
||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request method
|
||||
* @param config The OpenAPI configuration object
|
||||
* @param options The request options from the service
|
||||
* @param axiosClient The axios client instance to use
|
||||
* @returns CancelablePromise<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise<T> => {
|
||||
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||
try {
|
||||
const url = getUrl(config, options);
|
||||
const formData = getFormData(options);
|
||||
const body = getRequestBody(options);
|
||||
const headers = await getHeaders(config, options, formData);
|
||||
|
||||
if (!onCancel.isCancelled) {
|
||||
const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
|
||||
const responseBody = getResponseBody(response);
|
||||
const responseHeader = getResponseHeader(response, options.responseHeader);
|
||||
|
||||
const result: ApiResult = {
|
||||
url,
|
||||
ok: isSuccess(response.status),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: responseHeader ?? responseBody,
|
||||
};
|
||||
|
||||
catchErrorCodes(options, result);
|
||||
|
||||
resolve(result.body);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
120
frontend/src/api/index.ts
Normal file
120
frontend/src/api/index.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export { ApiError } from './core/ApiError';
|
||||
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
||||
export { OpenAPI } from './core/OpenAPI';
|
||||
export type { OpenAPIConfig } from './core/OpenAPI';
|
||||
|
||||
export type { ComboBoxItem } from './models/ComboBoxItem';
|
||||
export type { ComboBoxOut } from './models/ComboBoxOut';
|
||||
export type { CustomWebhook } from './models/CustomWebhook';
|
||||
export type { DispatchIn } from './models/DispatchIn';
|
||||
export type { GeneralConfig } from './models/GeneralConfig';
|
||||
export type { GeneralConfig_Game } from './models/GeneralConfig_Game';
|
||||
export type { GeneralConfig_Info } from './models/GeneralConfig_Info';
|
||||
export type { GeneralConfig_Run } from './models/GeneralConfig_Run';
|
||||
export type { GeneralConfig_Script } from './models/GeneralConfig_Script';
|
||||
export type { GeneralUserConfig_Data } from './models/GeneralUserConfig_Data';
|
||||
export type { GeneralUserConfig_Info } from './models/GeneralUserConfig_Info';
|
||||
export type { GeneralUserConfig_Input } from './models/GeneralUserConfig_Input';
|
||||
export type { GeneralUserConfig_Output } from './models/GeneralUserConfig_Output';
|
||||
export { GetStageIn } from './models/GetStageIn';
|
||||
export type { GlobalConfig_Function } from './models/GlobalConfig_Function';
|
||||
export type { GlobalConfig_Input } from './models/GlobalConfig_Input';
|
||||
export type { GlobalConfig_Notify } from './models/GlobalConfig_Notify';
|
||||
export type { GlobalConfig_Output } from './models/GlobalConfig_Output';
|
||||
export type { GlobalConfig_Start } from './models/GlobalConfig_Start';
|
||||
export type { GlobalConfig_UI } from './models/GlobalConfig_UI';
|
||||
export type { GlobalConfig_Update } from './models/GlobalConfig_Update';
|
||||
export type { GlobalConfig_Voice } from './models/GlobalConfig_Voice';
|
||||
export type { HistoryData } from './models/HistoryData';
|
||||
export type { HistoryDataGetIn } from './models/HistoryDataGetIn';
|
||||
export type { HistoryDataGetOut } from './models/HistoryDataGetOut';
|
||||
export { HistoryIndexItem } from './models/HistoryIndexItem';
|
||||
export { HistorySearchIn } from './models/HistorySearchIn';
|
||||
export type { HistorySearchOut } from './models/HistorySearchOut';
|
||||
export type { HTTPValidationError } from './models/HTTPValidationError';
|
||||
export type { InfoOut } from './models/InfoOut';
|
||||
export type { MaaConfig } from './models/MaaConfig';
|
||||
export type { MaaConfig_Info } from './models/MaaConfig_Info';
|
||||
export type { MaaConfig_Run } from './models/MaaConfig_Run';
|
||||
export type { MaaPlanConfig } from './models/MaaPlanConfig';
|
||||
export type { MaaPlanConfig_Info } from './models/MaaPlanConfig_Info';
|
||||
export type { MaaPlanConfig_Item } from './models/MaaPlanConfig_Item';
|
||||
export type { MaaUserConfig_Data } from './models/MaaUserConfig_Data';
|
||||
export type { MaaUserConfig_Info } from './models/MaaUserConfig_Info';
|
||||
export type { MaaUserConfig_Input } from './models/MaaUserConfig_Input';
|
||||
export type { MaaUserConfig_Output } from './models/MaaUserConfig_Output';
|
||||
export type { MaaUserConfig_Task } from './models/MaaUserConfig_Task';
|
||||
export type { NoticeOut } from './models/NoticeOut';
|
||||
export type { OutBase } from './models/OutBase';
|
||||
export type { PlanCreateIn } from './models/PlanCreateIn';
|
||||
export type { PlanCreateOut } from './models/PlanCreateOut';
|
||||
export type { PlanDeleteIn } from './models/PlanDeleteIn';
|
||||
export type { PlanGetIn } from './models/PlanGetIn';
|
||||
export type { PlanGetOut } from './models/PlanGetOut';
|
||||
export type { PlanIndexItem } from './models/PlanIndexItem';
|
||||
export type { PlanReorderIn } from './models/PlanReorderIn';
|
||||
export type { PlanUpdateIn } from './models/PlanUpdateIn';
|
||||
export { PowerIn } from './models/PowerIn';
|
||||
export type { QueueConfig } from './models/QueueConfig';
|
||||
export type { QueueConfig_Info } from './models/QueueConfig_Info';
|
||||
export type { QueueCreateOut } from './models/QueueCreateOut';
|
||||
export type { QueueDeleteIn } from './models/QueueDeleteIn';
|
||||
export type { QueueGetIn } from './models/QueueGetIn';
|
||||
export type { QueueGetOut } from './models/QueueGetOut';
|
||||
export type { QueueIndexItem } from './models/QueueIndexItem';
|
||||
export type { QueueItem } from './models/QueueItem';
|
||||
export type { QueueItem_Info } from './models/QueueItem_Info';
|
||||
export type { QueueItemCreateOut } from './models/QueueItemCreateOut';
|
||||
export type { QueueItemDeleteIn } from './models/QueueItemDeleteIn';
|
||||
export type { QueueItemGetIn } from './models/QueueItemGetIn';
|
||||
export type { QueueItemGetOut } from './models/QueueItemGetOut';
|
||||
export type { QueueItemIndexItem } from './models/QueueItemIndexItem';
|
||||
export type { QueueItemReorderIn } from './models/QueueItemReorderIn';
|
||||
export type { QueueItemUpdateIn } from './models/QueueItemUpdateIn';
|
||||
export type { QueueReorderIn } from './models/QueueReorderIn';
|
||||
export type { QueueSetInBase } from './models/QueueSetInBase';
|
||||
export type { QueueUpdateIn } from './models/QueueUpdateIn';
|
||||
export { ScriptCreateIn } from './models/ScriptCreateIn';
|
||||
export type { ScriptCreateOut } from './models/ScriptCreateOut';
|
||||
export type { ScriptDeleteIn } from './models/ScriptDeleteIn';
|
||||
export type { ScriptFileIn } from './models/ScriptFileIn';
|
||||
export type { ScriptGetIn } from './models/ScriptGetIn';
|
||||
export type { ScriptGetOut } from './models/ScriptGetOut';
|
||||
export { ScriptIndexItem } from './models/ScriptIndexItem';
|
||||
export type { ScriptReorderIn } from './models/ScriptReorderIn';
|
||||
export type { ScriptUpdateIn } from './models/ScriptUpdateIn';
|
||||
export type { ScriptUploadIn } from './models/ScriptUploadIn';
|
||||
export type { ScriptUrlIn } from './models/ScriptUrlIn';
|
||||
export type { SettingGetOut } from './models/SettingGetOut';
|
||||
export type { SettingUpdateIn } from './models/SettingUpdateIn';
|
||||
export { TaskCreateIn } from './models/TaskCreateIn';
|
||||
export type { TaskCreateOut } from './models/TaskCreateOut';
|
||||
export type { TimeSet } from './models/TimeSet';
|
||||
export type { TimeSet_Info } from './models/TimeSet_Info';
|
||||
export type { TimeSetCreateOut } from './models/TimeSetCreateOut';
|
||||
export type { TimeSetDeleteIn } from './models/TimeSetDeleteIn';
|
||||
export type { TimeSetGetIn } from './models/TimeSetGetIn';
|
||||
export type { TimeSetGetOut } from './models/TimeSetGetOut';
|
||||
export type { TimeSetIndexItem } from './models/TimeSetIndexItem';
|
||||
export type { TimeSetReorderIn } from './models/TimeSetReorderIn';
|
||||
export type { TimeSetUpdateIn } from './models/TimeSetUpdateIn';
|
||||
export type { UpdateCheckIn } from './models/UpdateCheckIn';
|
||||
export type { UpdateCheckOut } from './models/UpdateCheckOut';
|
||||
export type { UserConfig_Notify } from './models/UserConfig_Notify';
|
||||
export type { UserCreateOut } from './models/UserCreateOut';
|
||||
export type { UserDeleteIn } from './models/UserDeleteIn';
|
||||
export type { UserGetIn } from './models/UserGetIn';
|
||||
export type { UserGetOut } from './models/UserGetOut';
|
||||
export type { UserInBase } from './models/UserInBase';
|
||||
export { UserIndexItem } from './models/UserIndexItem';
|
||||
export type { UserReorderIn } from './models/UserReorderIn';
|
||||
export type { UserSetIn } from './models/UserSetIn';
|
||||
export type { UserUpdateIn } from './models/UserUpdateIn';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
export type { VersionOut } from './models/VersionOut';
|
||||
|
||||
export { Service } from './services/Service';
|
||||
100
frontend/src/api/mirrors.ts
Normal file
100
frontend/src/api/mirrors.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 镜像源 API 接口
|
||||
* 用于从后端获取最新的镜像源配置
|
||||
*/
|
||||
|
||||
import { OpenAPI } from '@/api'
|
||||
import type { MirrorConfig } from '@/config/mirrors'
|
||||
|
||||
export interface MirrorApiResponse {
|
||||
git?: MirrorConfig[]
|
||||
python?: MirrorConfig[]
|
||||
pip?: MirrorConfig[]
|
||||
apiEndpoints?: {
|
||||
local?: string
|
||||
production?: string
|
||||
proxy?: string
|
||||
}
|
||||
downloadLinks?: {
|
||||
[category: string]: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取镜像源配置
|
||||
*/
|
||||
export async function fetchMirrorConfig(): Promise<MirrorApiResponse> {
|
||||
try {
|
||||
const response = await fetch(`${OpenAPI.BASE}/api/mirrors`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.warn('获取镜像源配置失败,使用默认配置:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试镜像源连通性
|
||||
*/
|
||||
export async function testMirrorConnectivity(url: string, timeout: number = 5000): Promise<{
|
||||
success: boolean
|
||||
speed: number
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
signal: controller.signal,
|
||||
cache: 'no-cache',
|
||||
mode: 'no-cors' // 避免 CORS 问题
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
const speed = Date.now() - startTime
|
||||
|
||||
return {
|
||||
success: true,
|
||||
speed
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
speed: 9999,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量测试镜像源
|
||||
*/
|
||||
export async function batchTestMirrors(mirrors: MirrorConfig[]): Promise<MirrorConfig[]> {
|
||||
const promises = mirrors.map(async (mirror) => {
|
||||
const result = await testMirrorConnectivity(mirror.url)
|
||||
return {
|
||||
...mirror,
|
||||
speed: result.speed
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(promises)
|
||||
|
||||
// 按速度排序
|
||||
return results.sort((a, b) => (a.speed || 9999) - (b.speed || 9999))
|
||||
}
|
||||
15
frontend/src/api/models/ComboBoxItem.ts
Normal file
15
frontend/src/api/models/ComboBoxItem.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ComboBoxItem = {
|
||||
/**
|
||||
* 展示值
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* 实际值
|
||||
*/
|
||||
value: (string | null);
|
||||
};
|
||||
|
||||
24
frontend/src/api/models/ComboBoxOut.ts
Normal file
24
frontend/src/api/models/ComboBoxOut.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ComboBoxItem } from './ComboBoxItem';
|
||||
export type ComboBoxOut = {
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
code?: number;
|
||||
/**
|
||||
* 操作状态
|
||||
*/
|
||||
status?: string;
|
||||
/**
|
||||
* 操作消息
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* 下拉框选项
|
||||
*/
|
||||
data: Array<ComboBoxItem>;
|
||||
};
|
||||
|
||||
35
frontend/src/api/models/CustomWebhook.ts
Normal file
35
frontend/src/api/models/CustomWebhook.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CustomWebhook = {
|
||||
/**
|
||||
* Webhook唯一标识
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Webhook名称
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Webhook URL
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* 消息模板
|
||||
*/
|
||||
template: string;
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* 自定义请求头
|
||||
*/
|
||||
headers?: (Record<string, string> | null);
|
||||
/**
|
||||
* 请求方法
|
||||
*/
|
||||
method?: ('POST' | 'GET' | null);
|
||||
};
|
||||
|
||||
11
frontend/src/api/models/DispatchIn.ts
Normal file
11
frontend/src/api/models/DispatchIn.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DispatchIn = {
|
||||
/**
|
||||
* 目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID
|
||||
*/
|
||||
taskId: string;
|
||||
};
|
||||
|
||||
27
frontend/src/api/models/GeneralConfig.ts
Normal file
27
frontend/src/api/models/GeneralConfig.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { GeneralConfig_Game } from './GeneralConfig_Game';
|
||||
import type { GeneralConfig_Info } from './GeneralConfig_Info';
|
||||
import type { GeneralConfig_Run } from './GeneralConfig_Run';
|
||||
import type { GeneralConfig_Script } from './GeneralConfig_Script';
|
||||
export type GeneralConfig = {
|
||||
/**
|
||||
* 脚本基础信息
|
||||
*/
|
||||
Info?: (GeneralConfig_Info | null);
|
||||
/**
|
||||
* 脚本配置
|
||||
*/
|
||||
Script?: (GeneralConfig_Script | null);
|
||||
/**
|
||||
* 游戏配置
|
||||
*/
|
||||
Game?: (GeneralConfig_Game | null);
|
||||
/**
|
||||
* 运行配置
|
||||
*/
|
||||
Run?: (GeneralConfig_Run | null);
|
||||
};
|
||||
|
||||
31
frontend/src/api/models/GeneralConfig_Game.ts
Normal file
31
frontend/src/api/models/GeneralConfig_Game.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GeneralConfig_Game = {
|
||||
/**
|
||||
* 游戏/模拟器相关功能是否启用
|
||||
*/
|
||||
Enabled?: (boolean | null);
|
||||
/**
|
||||
* 类型: 模拟器, PC端
|
||||
*/
|
||||
Type?: ('Emulator' | 'Client' | null);
|
||||
/**
|
||||
* 游戏/模拟器程序路径
|
||||
*/
|
||||
Path?: (string | null);
|
||||
/**
|
||||
* 游戏/模拟器启动参数
|
||||
*/
|
||||
Arguments?: (string | null);
|
||||
/**
|
||||
* 游戏/模拟器等待启动时间
|
||||
*/
|
||||
WaitTime?: (number | null);
|
||||
/**
|
||||
* 是否强制关闭游戏/模拟器进程
|
||||
*/
|
||||
IfForceClose?: (boolean | null);
|
||||
};
|
||||
|
||||
15
frontend/src/api/models/GeneralConfig_Info.ts
Normal file
15
frontend/src/api/models/GeneralConfig_Info.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GeneralConfig_Info = {
|
||||
/**
|
||||
* 脚本名称
|
||||
*/
|
||||
Name?: (string | null);
|
||||
/**
|
||||
* 脚本根目录
|
||||
*/
|
||||
RootPath?: (string | null);
|
||||
};
|
||||
|
||||
19
frontend/src/api/models/GeneralConfig_Run.ts
Normal file
19
frontend/src/api/models/GeneralConfig_Run.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GeneralConfig_Run = {
|
||||
/**
|
||||
* 每日代理次数限制
|
||||
*/
|
||||
ProxyTimesLimit?: (number | null);
|
||||
/**
|
||||
* 重试次数限制
|
||||
*/
|
||||
RunTimesLimit?: (number | null);
|
||||
/**
|
||||
* 日志超时限制
|
||||
*/
|
||||
RunTimeLimit?: (number | null);
|
||||
};
|
||||
|
||||
58
frontend/src/api/models/GeneralConfig_Script.ts
Normal file
58
frontend/src/api/models/GeneralConfig_Script.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GeneralConfig_Script = {
|
||||
/**
|
||||
* 脚本可执行文件路径
|
||||
*/
|
||||
ScriptPath?: string | null
|
||||
/**
|
||||
* 脚本启动附加命令参数
|
||||
*/
|
||||
Arguments?: string | null
|
||||
/**
|
||||
* 是否追踪脚本子进程
|
||||
*/
|
||||
IfTrackProcess?: boolean | null
|
||||
/**
|
||||
* 配置文件路径
|
||||
*/
|
||||
ConfigPath?: string | null
|
||||
/**
|
||||
* 配置文件类型: 单个文件, 文件夹
|
||||
*/
|
||||
ConfigPathMode?: 'File' | 'Folder' | null
|
||||
/**
|
||||
* 更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时
|
||||
*/
|
||||
UpdateConfigMode?: 'Never' | 'Success' | 'Failure' | 'Always' | null
|
||||
/**
|
||||
* 日志文件路径
|
||||
*/
|
||||
LogPath?: string | null
|
||||
/**
|
||||
* 日志文件名格式
|
||||
*/
|
||||
LogPathFormat?: string | null
|
||||
/**
|
||||
* 日志时间戳开始位置
|
||||
*/
|
||||
LogTimeStart?: number | null
|
||||
/**
|
||||
* 日志时间戳结束位置
|
||||
*/
|
||||
LogTimeEnd?: number | null
|
||||
/**
|
||||
* 日志时间戳格式
|
||||
*/
|
||||
LogTimeFormat?: string | null
|
||||
/**
|
||||
* 成功时日志
|
||||
*/
|
||||
SuccessLog?: string | null
|
||||
/**
|
||||
* 错误时日志
|
||||
*/
|
||||
ErrorLog?: string | null
|
||||
}
|
||||
15
frontend/src/api/models/GeneralUserConfig_Data.ts
Normal file
15
frontend/src/api/models/GeneralUserConfig_Data.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GeneralUserConfig_Data = {
|
||||
/**
|
||||
* 上次代理日期
|
||||
*/
|
||||
LastProxyDate?: (string | null);
|
||||
/**
|
||||
* 代理次数
|
||||
*/
|
||||
ProxyTimes?: (number | null);
|
||||
};
|
||||
|
||||
39
frontend/src/api/models/GeneralUserConfig_Info.ts
Normal file
39
frontend/src/api/models/GeneralUserConfig_Info.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GeneralUserConfig_Info = {
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
Name?: (string | null);
|
||||
/**
|
||||
* 用户状态
|
||||
*/
|
||||
Status?: (boolean | null);
|
||||
/**
|
||||
* 剩余天数
|
||||
*/
|
||||
RemainedDay?: (number | null);
|
||||
/**
|
||||
* 是否在任务前执行脚本
|
||||
*/
|
||||
IfScriptBeforeTask?: (boolean | null);
|
||||
/**
|
||||
* 任务前脚本路径
|
||||
*/
|
||||
ScriptBeforeTask?: (string | null);
|
||||
/**
|
||||
* 是否在任务后执行脚本
|
||||
*/
|
||||
IfScriptAfterTask?: (boolean | null);
|
||||
/**
|
||||
* 任务后脚本路径
|
||||
*/
|
||||
ScriptAfterTask?: (string | null);
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
Notes?: (string | null);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user