Compare commits
1110 Commits
v2.1.4
...
v5.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33ee5c05a7 | ||
|
|
7952e88885 | ||
|
|
c1f7ea6922 | ||
|
|
196aa9873b | ||
|
|
334c17c055 | ||
|
|
0a20ee299d | ||
| 7d1ceda958 | |||
| 2f07c72025 | |||
|
|
5da96242aa | ||
|
|
5069a65559 | ||
| 21d1874602 | |||
|
|
1d91204842 | ||
|
|
3d204980a2 | ||
|
|
1eae8c80e5 | ||
|
|
a72ce489bd | ||
|
|
efa9f28aad | ||
|
|
4beed5048a | ||
|
|
e286fc8d55 | ||
|
|
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 | ||
|
|
c99707ecb4 | ||
|
|
2b8e648fe6 | ||
|
|
fcf61fd39a | ||
|
|
8e3026f91e | ||
|
|
8e00676faf | ||
|
|
ae293c4c20 | ||
| df4a5f3318 | |||
|
|
1da96c4d1d | ||
|
|
144c7f5db7 | ||
|
|
b3a3ccfea3 | ||
|
|
c3212f0ca1 | ||
|
|
a946999782 | ||
|
|
284c41feb7 | ||
|
|
ddf905cb13 | ||
|
|
d5082d18ef | ||
|
|
af51831062 | ||
|
|
fe4707df84 | ||
|
|
292e7f51e0 | ||
|
|
8936b1c41d | ||
|
|
d45da439bd | ||
|
|
7dc057e30f | ||
|
|
eb2f9d2cea | ||
|
|
fb1895c4eb | ||
|
|
90d3dad8c8 | ||
|
|
de12339c3e | ||
|
|
f07cd2b44a | ||
|
|
c7fbbf6f50 | ||
|
|
de262ee6bd | ||
|
|
0c123e9389 | ||
|
|
d13fbb063d | ||
|
|
5c24eb7d56 | ||
|
|
6c2f19a884 | ||
|
|
4ff632ed2a | ||
|
|
d445c0054f | ||
|
|
748fa7a004 | ||
|
|
c3e710b5cf | ||
|
|
a93a60d125 | ||
|
|
07f24c6168 | ||
|
|
7f5478b098 | ||
|
|
fb7a429ff2 | ||
| 3307793a3d | |||
|
|
0da9f4b7ab | ||
|
|
f45dc3a34c | ||
|
|
1c17f3d878 | ||
|
|
75e4d2b290 | ||
|
|
32fe941735 | ||
|
|
27633b1017 | ||
|
|
c34ca0dea9 | ||
|
|
0574e9c6cb | ||
|
|
b7f09141f1 | ||
|
|
022e59e65c | ||
|
|
a0731331a8 | ||
|
|
4b01222648 | ||
|
|
cae4b26c89 | ||
|
|
427c2332f5 | ||
|
|
6f0aec329b | ||
|
|
4e4d1d068f | ||
|
|
074f4f2ca9 | ||
|
|
c51f9ad901 | ||
|
|
792452c048 | ||
|
|
662eb0bc7f | ||
|
|
94a9bdbb93 | ||
|
|
df96183f42 | ||
|
|
a5b4f6f59f | ||
|
|
6f7497cbe9 | ||
|
|
dbdc2144b7 | ||
|
|
e34106f857 | ||
|
|
c3c07804cd | ||
|
|
0b5ac6bb6e | ||
|
|
ea87eefb9b | ||
|
|
20dc4656dc | ||
|
|
3f0f1612e3 | ||
|
|
e92b6ecfe6 | ||
|
|
37502b6fd8 | ||
|
|
8c1c3c5675 | ||
|
|
b4228e3f35 | ||
|
|
e78f3973be | ||
|
|
29536003a4 | ||
|
|
ffa3767198 | ||
|
|
8702070725 | ||
|
|
793259a194 | ||
|
|
3a63a73244 | ||
|
|
43448cc68d | ||
|
|
f0d272cce5 | ||
|
|
9983455b60 | ||
|
|
0a411c150a | ||
|
|
89b49a1143 | ||
|
|
917454c0b9 | ||
|
|
170b87e7a8 | ||
|
|
68db248a7e | ||
|
|
24614099ed | ||
|
|
8bbfdcbc04 | ||
|
|
126799d2a2 | ||
|
|
c625354dec | ||
|
|
7e08c88a3e | ||
|
|
764d0afb50 | ||
|
|
fe87547406 | ||
|
|
a1fd27722b | ||
|
|
3bd6611cd5 | ||
|
|
f3e1b4580a | ||
|
|
449d8a032e | ||
|
|
b40fa35622 | ||
|
|
ff7e433634 | ||
|
|
1ea9d10bb4 | ||
|
|
04fd2b10fa | ||
|
|
e79417ec5e | ||
|
|
684211c129 | ||
|
|
f94e129cba | ||
|
|
87a140d373 | ||
|
|
f135a6510f | ||
|
|
6960184911 | ||
|
|
7bd270c662 | ||
|
|
f54e83673f | ||
|
|
ee40fdb3c3 | ||
|
|
52ebf7b027 | ||
|
|
85891dc918 | ||
|
|
7348a87a20 | ||
|
|
1dfa3e3f44 | ||
|
|
37ced2e535 | ||
|
|
11876acc62 | ||
|
|
756c0926ec | ||
|
|
9604fc9a8e | ||
|
|
5bcc527889 | ||
|
|
0d616289ed | ||
|
|
a8473b6a04 | ||
|
|
e1352586b7 | ||
|
|
849d5f18eb | ||
|
|
77298f4dab | ||
|
|
b7a2b045fb | ||
|
|
c7072da81d | ||
|
|
4c9b9fb74a | ||
|
|
feb4b516a4 | ||
|
|
d50191c6b8 | ||
|
|
3db3fa4e1f | ||
|
|
ad31961c2b | ||
|
|
ccdaefeb61 | ||
|
|
8c9bcba198 | ||
|
|
a273af0abe | ||
|
|
05a063b001 | ||
|
|
8487195512 | ||
|
|
250c47ccb7 | ||
|
|
e5aeb4f3a7 | ||
|
|
eab8d984e6 | ||
|
|
6b3f19a618 | ||
|
|
d5f8871064 | ||
|
|
77e899957c | ||
|
|
dd3515305c | ||
|
|
73c3ec4820 | ||
|
|
fde5160d56 | ||
|
|
13923705e5 | ||
|
|
d3afc95261 | ||
|
|
326d7e474c | ||
|
|
537b5d9725 | ||
|
|
697c1b5b43 | ||
|
|
2b6057161e | ||
|
|
8e2a6444bd | ||
|
|
b04df40b7d | ||
|
|
3e17d94904 | ||
|
|
29123054cc | ||
|
|
530c038985 | ||
|
|
2e9d2f0491 | ||
|
|
69ab9b4f15 | ||
|
|
1c8b940925 | ||
|
|
0e05a4ea18 | ||
|
|
9e4285b4c5 | ||
|
|
e89d2af8ae | ||
|
|
8e7d663060 | ||
|
|
179b33387e | ||
|
|
2c9a7c443f | ||
|
|
0de964e68c | ||
|
|
97046af931 | ||
|
|
ec24e776e7 | ||
|
|
92d4ef05fa | ||
|
|
fb27c50c75 | ||
|
|
0141bf039a | ||
|
|
54f9bb7f21 | ||
|
|
2c4e5eb5cc | ||
|
|
cd42f45a1f | ||
|
|
ec38895765 | ||
|
|
fc30579bbc | ||
|
|
3e6223e4e5 | ||
|
|
27c71124ff | ||
|
|
3a8a8a36f5 | ||
|
|
ed6de5e806 | ||
|
|
bf8a5befa3 | ||
|
|
a3fe641f6b | ||
|
|
f4f99c25db | ||
|
|
640d80e334 | ||
|
|
4f196b516f | ||
|
|
87f07bf95c | ||
|
|
1bfd7891b5 | ||
|
|
472eb0ee52 | ||
|
|
7638c67da6 | ||
|
|
7a0c143b5b | ||
|
|
fff8e11524 | ||
|
|
af24208cf3 | ||
|
|
85bccea2dd | ||
|
|
f7c8cac6ec | ||
|
|
97a5ac5bb0 | ||
|
|
1d57076010 | ||
|
|
d298ac872c | ||
|
|
c0581e781c | ||
|
|
6befd6341a | ||
|
|
504ef8dd68 | ||
|
|
127500d890 | ||
|
|
ab94173df7 | ||
|
|
e50697aae1 | ||
|
|
c71389fc0b | ||
|
|
732129ba34 | ||
|
|
77dfc895ed | ||
|
|
0c6f3123f8 | ||
|
|
e872d30b3a | ||
|
|
48fa4413c8 | ||
|
|
94e693db77 | ||
|
|
025d328d79 | ||
|
|
d086a5a152 | ||
|
|
5f2337d949 | ||
|
|
5e163181a8 | ||
|
|
0913cbb813 | ||
|
|
6aed64ce4e | ||
|
|
3c95e58fb2 | ||
|
|
64d0d4ed41 | ||
|
|
7fb659c511 | ||
|
|
d4ec91940f | ||
|
|
77f63ebb44 | ||
|
|
051b062e29 | ||
|
|
ec9083b556 | ||
|
|
156080a3b7 | ||
|
|
5b6effd43e | ||
|
|
97e8d41c39 | ||
|
|
d64ef447d2 | ||
|
|
461b2a62b0 | ||
|
|
f8988651b5 | ||
|
|
ffe865e29e | ||
|
|
cd5250c09f | ||
|
|
8f01bf6027 | ||
|
|
81eb11f5c5 | ||
|
|
80cadfa52c | ||
|
|
9974d70d99 | ||
|
|
6e3e4662f4 | ||
|
|
e2fb2e5565 | ||
|
|
6f8be226b0 | ||
|
|
2606f1e587 | ||
|
|
4d1fd7ea76 | ||
|
|
e61fbe6c69 | ||
|
|
0c287577ca | ||
|
|
a4aa562db9 | ||
|
|
33c5ff3a52 | ||
|
|
7c8e43bd35 | ||
|
|
bc0014dbe6 | ||
|
|
e41eca33bc | ||
|
|
e39b965459 | ||
|
|
f2aa3d7347 | ||
|
|
76c126284f | ||
|
|
44ee917d88 | ||
|
|
669019c051 | ||
|
|
4685c12570 | ||
|
|
d815529510 | ||
|
|
1da3620feb | ||
|
|
44d529c60f | ||
|
|
cdbcebd945 | ||
|
|
5bc2bf9397 | ||
|
|
d41419a579 | ||
|
|
a7e15e509e | ||
|
|
6518a378ac | ||
|
|
ebb73182f7 | ||
|
|
8fcc69165f | ||
|
|
5b98c58926 | ||
|
|
ce1534657b | ||
|
|
e2f02ea616 | ||
|
|
76134c2e2b | ||
|
|
2010855139 | ||
|
|
e550b9a155 | ||
|
|
53049d4acd | ||
|
|
bb4b45bc36 |
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
|
||||
BIN
AUTO_MAA.exe
BIN
AUTO_MAA.exe
Binary file not shown.
76
AUTO_MAA.py
76
AUTO_MAA.py
@@ -1,76 +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
|
||||
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import atexit
|
||||
import datetime
|
||||
import time
|
||||
import os
|
||||
from termcolor import colored
|
||||
|
||||
#资源回收
|
||||
def cleanup():
|
||||
if os.path.exists("state/BEGIN"):
|
||||
os.remove("state/BEGIN")
|
||||
|
||||
DATABASE="data/data.db"
|
||||
|
||||
#设置回调函数
|
||||
atexit.register(cleanup)
|
||||
while True:
|
||||
db=sqlite3.connect(DATABASE)
|
||||
cur=db.cursor()
|
||||
cur.execute("SELECT * FROM timeset WHERE True")
|
||||
timeset=cur.fetchall()
|
||||
cur.close()
|
||||
db.close()
|
||||
timeset=[list(row) for row in timeset]
|
||||
timeset=[timeset[i][0] for i in range(len(timeset))]
|
||||
for i in range(60):
|
||||
#展示当前信息
|
||||
curtime=datetime.datetime.now().strftime("%H:%M")
|
||||
os.system('cls')
|
||||
if len(timeset)!=0:
|
||||
print(colored("设定时间:"+','.join(timeset),'green'))
|
||||
print(colored("当前时间:"+curtime,'green'))
|
||||
print(colored("运行日志:",'green'))
|
||||
if os.path.exists("state/running"):
|
||||
print(colored("正在运行代理",'yellow'))
|
||||
elif os.path.exists("log.txt"):
|
||||
with open("log.txt",'r',encoding="utf-8") as f:
|
||||
linex=f.read()
|
||||
print(colored(linex,'light_green'))
|
||||
else:
|
||||
print(colored("暂无",'light_green'))
|
||||
#定时执行
|
||||
if (curtime in timeset) and not os.path.exists("state/running"):
|
||||
with open("state/BEGIN","w",encoding="utf-8") as f:
|
||||
print("BEGIN",file=f)
|
||||
run=subprocess.Popen(["run.exe"])
|
||||
runpid=run.pid
|
||||
while True:
|
||||
if os.path.exists("state/END"):
|
||||
os.system('taskkill /F /T /PID '+str(runpid))
|
||||
os.remove("state/END")
|
||||
break
|
||||
time.sleep(1)
|
||||
os.remove("state/BEGIN")
|
||||
time.sleep(1)
|
||||
272
README.md
272
README.md
@@ -1,270 +1,2 @@
|
||||
# AUTO_MAA
|
||||
MAA多账号管理与自动化软件
|
||||
|
||||

|
||||
|
||||
----------------------------------------------------------------------------------------------
|
||||
|
||||
## 重要声明
|
||||
本软件是一个外部工具,旨在优化MAA多账号功能体验。该软件包可以存储明日方舟多账号数据,并通过修改MAA配置文件、读取MAA日志等行为自动完成多账号代理。本开发团队承诺,不会修改明日方舟游戏本体与相关配置文件。
|
||||
|
||||
本项目使用GPL开源,相关细则如下:
|
||||
|
||||
- **作者:** AUTO_MAA软件作者为DLmaster、DLmaster361或DLmaster_361,以上均指代同一人。
|
||||
- **使用:** AUTO_MAA使用者可以按自己的意愿自由使用本软件。依据GPL,对于由此可能产生的损失,AUTO_MAA项目组不负任何责任
|
||||
- **分发:** AUTO_MAA允许任何人自由分发本软件,包括进行商业活动牟利。但所有分发者必须遵循GPL向接收者提供本软件项目地址、完整的软件源码与GPL协议原文(件),违反者可能会被追究法律责任
|
||||
- **传播:** AUTO_MAA原则上允许传播者自由传播本软件。但由于软件性质,项目组不希望发现任何人在明日方舟官方媒体(包括官方媒体账号与森空岛社区等)或明日方舟游戏相关内容(包括同好群、线下活动与游戏内容讨论等)下提及AUTO_MAA或MAA,希望各位理解
|
||||
- **衍生:** AUTO_MAA允许任何人对软件本体或软件部分代码进行二次开发或利用。但依据GPL,相关成果也必须使用GPL开源
|
||||
- **授权:** 如果希望在使用AUTO_MAA的相关成果后仍保持自己的项目闭源,请在Issues中说明来意。得到项目组认可后,我们可以提供另一份使用不同协议的代码,此协议主要内容如下:被授权者可以自由使用该代码并维持闭源;被授权者必须定期为AUTO_MAA作出贡献
|
||||
- **贡献:** 不论是直接参与软件的维护编写,或是撰写文档、测试、反馈BUG、给出建议、参与讨论,都为AUTO_MAA项目的发展完善做出了不可忽视的贡献。项目组提倡各位贡献者遵照GitHub开源社区惯例,发布Issues参与项目。避免私信或私发邮件(安全性漏洞或敏感问题除外),以帮助更多用户
|
||||
|
||||
以上细则是本项目对GPL的相关补充与强调。未提及的以GPL为准,发生冲突的以GPL为准。如有不清楚的部分,请发Issues询问。若发生纠纷,相关内容也没有在Issues上提及的,项目组拥有最终解释权
|
||||
|
||||
**注意**
|
||||
- 由于本软件有修改其它目录JSON文件等行为,使用前请将AUTO_MAA添加入Windows Defender信任区以及防病毒软件的信任区或开发者目录,避免被误杀
|
||||
- 如程序无法正常启动,请删除`state`目录下所有文件后重试
|
||||
|
||||
## 安装与配置MAA
|
||||
|
||||
本软件是MAA的外部工具,需要安装配置MAA后才能使用。
|
||||
|
||||
### MAA安装
|
||||
|
||||
什么是MAA? [官网](https://maa.plus/)/[GitHub](https://github.com/CHNZYX/Auto_Simulated_Universe/archive/refs/heads/main.zip)
|
||||
|
||||
MAA下载地址 [GitHub下载](https://github.com/MaaAssistantArknights/MaaAssistantArknights/releases)
|
||||
|
||||
### MAA配置
|
||||
|
||||
1. 完成MAA的adb配置等基本配置
|
||||
|
||||
2. 确保当前配置名为“Default”,取消所有“定时执行”
|
||||
|
||||

|
||||
|
||||
3. 取消勾选“开机自启动MAA”,勾选“启动MAA后直接运行”和“启动MAA后自动开启模拟器”。配置自己模拟器所在的位置并根据实际情况填写“等待模拟器启动时间”(建议预留10s以防意外)。如果是多开用户,需要填写“附加命令”,具体填写值参见多开模拟器对应快捷方式路径(如“-v 1”)。
|
||||
|
||||

|
||||
|
||||
4. 勾选“定时检查更新”、“自动下载更新包”和“自动安装更新包”
|
||||
|
||||

|
||||
|
||||
## 下载AUTO_MAA软件包 [](https://github.com/DLmaster361/AUTO_MAA/releases)
|
||||
|
||||
GitHub下载地址 [GitHub下载](https://github.com/DLmaster361/AUTO_MAA/releases)
|
||||
|
||||
## 配置用户信息与相关参数
|
||||
|
||||
**注意:** 当前所有的密码输入部分都存在一点“小问题”,请在输入密码时避免输入Delete、F12、Tab等功能键。
|
||||
|
||||
-------------------------------------------------
|
||||
|
||||
### 第一次启动
|
||||
|
||||
双击启动`manage.exe`,输入MAA所在文件夹路径并回车(注意使用斜杠的种类,不要使用反斜杠),然后设置管理密钥(密钥可以包含字母大小写与特殊字符)。
|
||||
|
||||

|
||||
|
||||
管理密钥是解密用户密码的唯一凭证,与数据库绑定。密钥丢失或`data/key/`目录下任一文件损坏都将导致解密无法正常进行。
|
||||
|
||||
本项目采用自主开发的混合加密模式,项目组也无法找回您的管理密钥或修复`data/key/`目录下的文件。如果不幸的事发生,建议您删除`data/data.db`重新录入信息。
|
||||
|
||||
### 添加用户
|
||||
|
||||
输入“+”以开始添加用户。依次输入:
|
||||
|
||||
**用户名:** 管理用户的惟一凭证
|
||||
|
||||
**手机号码:** 允许隐去中间四位以“****”代替
|
||||
|
||||
**代理天数:** 这个还要我解释吗?
|
||||
|
||||

|
||||
|
||||
### 删除用户
|
||||
|
||||
输入用户名+“-”以删除用户。格式:
|
||||
|
||||
```
|
||||
用户名 -
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 配置用户状态
|
||||
|
||||
**启用代理:** 输入用户名+“y”以启用该用户的代理。格式:
|
||||
|
||||
```
|
||||
用户名 y
|
||||
```
|
||||
|
||||

|
||||
|
||||
**禁用代理:** 输入用户名+“n”以禁用该用户的代理。格式:
|
||||
|
||||
```
|
||||
用户名 n
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 续期
|
||||
|
||||
输入用户名+续期天数+“+”以延长该用户的代理天数。格式:
|
||||
|
||||
```
|
||||
用户名 续期天数 +
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 修改刷取关卡
|
||||
|
||||
输入用户名+关卡号+“~”以更改该用户的代理关卡。格式:
|
||||
|
||||
```
|
||||
用户名 关卡号 ~
|
||||
```
|
||||
|
||||

|
||||
|
||||
**特别的:**
|
||||
|
||||
你可以自定义关卡号替换方案。程序会读取`gameid.txt`中的数据,依据此进行关卡号的替换,便于常用关卡的使用。`gameid.txt`在初始已经存储了一些常用资源本的替代方案。
|
||||
|
||||

|
||||
|
||||
### 设置MAA路径
|
||||
|
||||
输入“/”+新的MAA文件夹路径以修改MAA安装位置的配置。格式:
|
||||
|
||||
```
|
||||
/新的MAA文件夹路径
|
||||
```
|
||||
|
||||
**注意:** ‘/’与路径间没有空格,路径同样不能使用反斜杠
|
||||
|
||||

|
||||
|
||||
### 设置启动时间
|
||||
|
||||
**添加启动时间:** 输入“:+”+时间以添加定时启动时间。格式:
|
||||
|
||||
```
|
||||
:+小时:分钟
|
||||
```
|
||||
|
||||
**注意:** 所有输入间没有空格
|
||||
|
||||

|
||||
|
||||
**删除启动时间:** 输入“:-”+时间以删除定时启动时间。格式:
|
||||
|
||||
```
|
||||
:-小时:分钟
|
||||
```
|
||||
|
||||
**注意:** 所有输入间没有空格
|
||||
|
||||

|
||||
|
||||
### 检索信息
|
||||
|
||||
**检索所有信息:** `manage.exe`打开时会打印所有用户与配置信息。除此之外,你可以通过输入“all ?”以打印所有信息,如下:
|
||||
|
||||
```
|
||||
all ?
|
||||
```
|
||||
|
||||

|
||||
|
||||
**检索MAA路径:** 输入“maa ?”以检索MAA安装路径,如下:
|
||||
|
||||
```
|
||||
maa ?
|
||||
```
|
||||
|
||||

|
||||
|
||||
**检索启动时间:** 输入“time ?”以检索定时启动的时间,如下:
|
||||
|
||||
```
|
||||
time ?
|
||||
```
|
||||
|
||||

|
||||
|
||||
**检索指定用户:** 输入用户名+“?”以检索指定用户信息,如下:
|
||||
|
||||
```
|
||||
用户名 ?
|
||||
```
|
||||
|
||||
**注意:** 由于需要检索用户密码,每一次`manage.exe`启动后的首次查询需要验证管理密钥。为了方便操作,之后的查询不会再要求重复验证。因此,完成密码查询后,请及时关闭`manage.exe`。
|
||||
|
||||

|
||||
|
||||
### 修改管理密钥
|
||||
|
||||
输入“*”以开始修改管理密钥。依次输入:
|
||||
|
||||
**旧管理密钥:** 当数据库中没有存储用户信息时,允许跳过验证直接配置新管理密钥
|
||||
|
||||
**新管理密钥:** 请妥善保管,丢失无法找回。
|
||||
|
||||

|
||||
|
||||
### 退出
|
||||
|
||||
输入“-”以退出`manage.exe`,如下:
|
||||
|
||||
```
|
||||
-
|
||||
```
|
||||
|
||||
## 运行代理
|
||||
|
||||
### 直接运行
|
||||
|
||||
双击`run.exe`直接运行
|
||||
|
||||
### 定时运行
|
||||
|
||||
双击`AUTO_MAA.exe`打开,不要关闭。它会读取设定时间,在该时刻自动运行
|
||||
|
||||
**注意:** 将优先进行剿灭代理
|
||||
|
||||
## 关于
|
||||
|
||||
项目图标由文心一格AI生成
|
||||
|
||||
----------------------------------------------------------------------------------------------
|
||||
|
||||
欢迎加入AUTO_MAA项目组,欢迎反馈bug
|
||||
|
||||
QQ群:暂时没有
|
||||
|
||||
----------------------------------------------------------------------------------------------
|
||||
|
||||
如果喜欢本项目,可以打赏送作者一杯咖啡喵!
|
||||
|
||||

|
||||
|
||||
----------------------------------------------------------------------------------------------
|
||||
## 贡献者
|
||||
|
||||
感谢以下贡献者对本项目做出的贡献
|
||||
|
||||
<a href="https://github.com/DLmaster361/AUTO_MAA/graphs/contributors">
|
||||
|
||||
<img src="https://contrib.rocks/image?repo=DLmaster361/AUTO_MAA" />
|
||||
|
||||
</a>
|
||||
|
||||

|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#DLmaster361/AUTO_MAA&Date)
|
||||
TEST
|
||||
TEST
|
||||
34
app/__init__.py
Normal file
34
app/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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 .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()
|
||||
198
app/api/setting.py
Normal file
198
app/api/setting.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# 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 *
|
||||
from app.models.config import Webhook as WebhookConfig
|
||||
|
||||
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/get",
|
||||
summary="查询 webhook 配置",
|
||||
response_model=WebhookGetOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut:
|
||||
|
||||
try:
|
||||
index, data = await Config.get_webhook(None, None, webhook.webhookId)
|
||||
index = [WebhookIndexItem(**_) for _ in index]
|
||||
data = {uid: Webhook(**cfg) for uid, cfg in data.items()}
|
||||
except Exception as e:
|
||||
return WebhookGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
index=[],
|
||||
data={},
|
||||
)
|
||||
return WebhookGetOut(index=index, data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/add",
|
||||
summary="添加定时项",
|
||||
response_model=WebhookCreateOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def add_webhook() -> WebhookCreateOut:
|
||||
|
||||
uid, config = await Config.add_webhook(None, None)
|
||||
data = Webhook(**(await config.toDict()))
|
||||
return WebhookCreateOut(webhookId=str(uid), data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/update", summary="更新定时项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def update_webhook(webhook: WebhookUpdateIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.update_webhook(
|
||||
None, None, webhook.webhookId, webhook.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(
|
||||
"/webhook/delete", summary="删除定时项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def delete_webhook(webhook: WebhookDeleteIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.del_webhook(None, None, webhook.webhookId)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/order", summary="重新排序定时项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def reorder_webhook(webhook: WebhookReorderIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.reorder_webhook(None, None, webhook.indexList)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/test", summary="测试Webhook配置", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def test_webhook(webhook: WebhookTestIn = Body(...)) -> OutBase:
|
||||
"""测试自定义Webhook"""
|
||||
|
||||
try:
|
||||
webhook_config = WebhookConfig()
|
||||
await webhook_config.load(webhook.data.model_dump())
|
||||
await Notify.WebhookPush(
|
||||
"AUTO-MAS Webhook测试",
|
||||
"这是一条测试消息,如果您收到此消息,说明Webhook配置正确!",
|
||||
webhook_config,
|
||||
)
|
||||
except Exception as e:
|
||||
return OutBase(code=500, status="error", message=f"Webhook测试失败: {str(e)}")
|
||||
return OutBase()
|
||||
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()
|
||||
41
app/core/__init__.py
Normal file
41
app/core/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# 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 .broadcast import Broadcast
|
||||
from .config import Config, MaaConfig, GeneralConfig, MaaUserConfig, GeneralUserConfig
|
||||
from .timer import MainTimer
|
||||
from .task_manager import TaskManager
|
||||
|
||||
__all__ = [
|
||||
"Broadcast",
|
||||
"Config",
|
||||
"MaaConfig",
|
||||
"GeneralConfig",
|
||||
"MainTimer",
|
||||
"TaskManager",
|
||||
"MaaUserConfig",
|
||||
"GeneralUserConfig",
|
||||
]
|
||||
52
app/core/broadcast.py
Normal file
52
app/core/broadcast.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# 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):
|
||||
"""向所有订阅者广播消息"""
|
||||
for subscriber in self.__subscribers:
|
||||
await subscriber.put(deepcopy(item))
|
||||
|
||||
|
||||
Broadcast = _Broadcast()
|
||||
1951
app/core/config.py
Normal file
1951
app/core/config.py
Normal file
File diff suppressed because it is too large
Load Diff
384
app/core/task_manager.py
Normal file
384
app/core/task_manager.py
Normal file
@@ -0,0 +1,384 @@
|
||||
# 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 uuid
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing import Dict, Optional, Literal
|
||||
|
||||
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
|
||||
|
||||
|
||||
logger = get_logger("业务调度")
|
||||
|
||||
|
||||
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 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]
|
||||
):
|
||||
|
||||
logger.info(f"开始运行任务: {task_id}, 模式: {mode}")
|
||||
|
||||
if mode == "设置脚本":
|
||||
|
||||
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
|
||||
|
||||
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_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:
|
||||
|
||||
# 初始化任务列表
|
||||
if task_id in Config.QueueConfig:
|
||||
|
||||
task_list = []
|
||||
for queue_item in Config.QueueConfig[task_id].QueueItem.values():
|
||||
if queue_item.get("Info", "ScriptId") == "-":
|
||||
continue
|
||||
script_uid = uuid.UUID(queue_item.get("Info", "ScriptId"))
|
||||
|
||||
task_list.append(
|
||||
{
|
||||
"script_id": str(script_uid),
|
||||
"status": "等待",
|
||||
"name": Config.ScriptConfig[script_uid].get("Info", "Name"),
|
||||
"user_list": [
|
||||
{
|
||||
"user_id": str(user_id),
|
||||
"status": "等待",
|
||||
"name": config.get("Info", "Name"),
|
||||
}
|
||||
for user_id, config in Config.ScriptConfig[
|
||||
script_uid
|
||||
].UserData.items()
|
||||
if config.get("Info", "Status")
|
||||
and config.get("Info", "RemainedDay") != 0
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
elif actual_id is not None and actual_id in Config.ScriptConfig:
|
||||
|
||||
task_list = [
|
||||
{
|
||||
"script_id": str(actual_id),
|
||||
"status": "等待",
|
||||
"name": Config.ScriptConfig[actual_id].get("Info", "Name"),
|
||||
"user_list": [
|
||||
{
|
||||
"user_id": str(user_id),
|
||||
"status": "等待",
|
||||
"name": config.get("Info", "Name"),
|
||||
}
|
||||
for user_id, config in Config.ScriptConfig[
|
||||
actual_id
|
||||
].UserData.items()
|
||||
if config.get("Info", "Status")
|
||||
and config.get("Info", "RemainedDay") != 0
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
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))
|
||||
)
|
||||
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:
|
||||
uid = uuid.UUID(task_id)
|
||||
if uid not in self.task_dict:
|
||||
raise ValueError("任务未在运行")
|
||||
self.task_dict[uid].cancel()
|
||||
|
||||
async def remove_task(
|
||||
self, task: asyncio.Task, mode: str, task_id: uuid.UUID
|
||||
) -> None:
|
||||
"""
|
||||
处理任务结束后的收尾工作
|
||||
|
||||
Parameters
|
||||
----------
|
||||
task : asyncio.Task
|
||||
任务对象
|
||||
mode : str
|
||||
任务模式
|
||||
task_id : uuid.UUID
|
||||
任务ID
|
||||
"""
|
||||
|
||||
logger.info(f"任务结束: {task_id}")
|
||||
|
||||
# 从任务字典中移除任务
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"任务 {task_id} 已结束")
|
||||
self.task_dict.pop(task_id)
|
||||
|
||||
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()
|
||||
147
app/core/timer.py
Normal file
147
app/core/timer.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# 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 keyboard
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
|
||||
|
||||
logger = get_logger("主业务定时器")
|
||||
|
||||
|
||||
class _MainTimer:
|
||||
|
||||
async def second_task(self):
|
||||
"""每秒定期任务"""
|
||||
logger.info("每秒定期任务启动")
|
||||
|
||||
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):
|
||||
"""定时启动代理任务"""
|
||||
|
||||
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
for uid, queue in Config.QueueConfig.items():
|
||||
|
||||
if not queue.get("Info", "TimeEnabled"):
|
||||
continue
|
||||
|
||||
# 避免重复调起任务
|
||||
if curtime == queue.get("Data", "LastTimedStart"):
|
||||
continue
|
||||
|
||||
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()
|
||||
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="TaskManager",
|
||||
type="Signal",
|
||||
data={"newTask": str(task_id)},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
@logger.catch()
|
||||
async def set_silence(self):
|
||||
"""静默模式通过模拟老板键来隐藏模拟器窗口"""
|
||||
|
||||
if (
|
||||
len(Config.if_ignore_silence) == 0
|
||||
and Config.get("Function", "IfSilence")
|
||||
and Config.get("Function", "BossKey") != ""
|
||||
):
|
||||
|
||||
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:
|
||||
keyboard.press_and_release(
|
||||
"+".join(
|
||||
_.strip().lower()
|
||||
for _ in Config.get("Function", "BossKey").split("+")
|
||||
)
|
||||
)
|
||||
logger.info(f"模拟按键: {Config.get('Function', 'BossKey')}")
|
||||
except Exception as e:
|
||||
logger.exception(f"模拟按键时出错: {e}")
|
||||
|
||||
|
||||
MainTimer = _MainTimer()
|
||||
934
app/models/ConfigBase.py
Normal file
934
app/models/ConfigBase.py
Normal file
@@ -0,0 +1,934 @@
|
||||
# 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 urllib.parse import urlparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Any, Dict, Union, Optional, TypeVar, Generic, Type
|
||||
|
||||
|
||||
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 __init__(self, tpye: type[dict] | type[list] = dict) -> None:
|
||||
self.type = tpye
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
try:
|
||||
data = json.loads(value)
|
||||
if isinstance(data, self.type):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except json.JSONDecodeError:
|
||||
return False
|
||||
|
||||
def correct(self, value: Any) -> str:
|
||||
return (
|
||||
value if self.validate(value) else ("{ }" if self.type == dict 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 URLValidator(ConfigValidator):
|
||||
"""URL格式验证器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schemes: list[str] | None = None,
|
||||
require_netloc: bool = True,
|
||||
default: str = "",
|
||||
):
|
||||
"""
|
||||
:param schemes: 允许的协议列表, 若为 None 则允许任意协议
|
||||
:param require_netloc: 是否要求必须包含网络位置, 如域名或IP
|
||||
"""
|
||||
self.schemes = [s.lower() for s in schemes] if schemes else None
|
||||
self.require_netloc = require_netloc
|
||||
self.default = default
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
|
||||
if value == self.default:
|
||||
return True
|
||||
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
|
||||
try:
|
||||
parsed = urlparse(value)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# 检查协议
|
||||
if self.schemes is not None:
|
||||
if not parsed.scheme or parsed.scheme.lower() not in self.schemes:
|
||||
return False
|
||||
else:
|
||||
# 不限制协议仍要求有 scheme
|
||||
if not parsed.scheme:
|
||||
return False
|
||||
|
||||
# 检查是否包含网络位置
|
||||
if self.require_netloc and not parsed.netloc:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def correct(self, value: Any) -> str:
|
||||
|
||||
if self.validate(value):
|
||||
return value
|
||||
|
||||
if isinstance(value, str):
|
||||
# 简单尝试:若看起来像域名,加上 https://
|
||||
stripped = value.strip()
|
||||
if stripped and not stripped.startswith(("http://", "https://")):
|
||||
candidate = f"https://{stripped}"
|
||||
if self.validate(candidate):
|
||||
return candidate
|
||||
|
||||
return self.default
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
T = TypeVar("T", bound="ConfigBase")
|
||||
|
||||
|
||||
class MultipleConfig(Generic[T]):
|
||||
"""
|
||||
多配置项管理类
|
||||
|
||||
这个类允许管理多个配置项实例, 可以添加、删除、修改配置项, 并将其保存到 JSON 文件中。
|
||||
允许通过 `config[uuid]` 访问配置项, 使用 `uuid in config` 检查是否存在配置项, 使用 `len(config)` 获取配置项数量。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sub_config_type: List[type]
|
||||
子配置项的类型列表, 必须是 ConfigBase 的子类
|
||||
"""
|
||||
|
||||
def __init__(self, sub_config_type: List[Type[T]]):
|
||||
|
||||
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: List[Type[T]] = sub_config_type
|
||||
self.file: Path | None = None
|
||||
self.order: List[uuid.UUID] = []
|
||||
self.data: Dict[uuid.UUID, T] = {}
|
||||
self.is_locked = False
|
||||
|
||||
def __getitem__(self, key: uuid.UUID) -> T:
|
||||
"""允许通过 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, ignore_multi_config: bool = False, if_decrypt: bool = True
|
||||
) -> 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(ignore_multi_config, if_decrypt)
|
||||
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[T]) -> tuple[uuid.UUID, T]:
|
||||
"""
|
||||
添加一个新的配置项
|
||||
|
||||
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
|
||||
31
app/models/__init__.py
Normal file
31
app/models/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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 .ConfigBase import *
|
||||
from .config import *
|
||||
from .schema import *
|
||||
|
||||
__all__ = ["ConfigBase", "config", "schema"]
|
||||
569
app/models/config.py
Normal file
569
app/models/config.py
Normal file
@@ -0,0 +1,569 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# 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 pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .ConfigBase import *
|
||||
|
||||
|
||||
class Webhook(ConfigBase):
|
||||
"""Webhook 配置"""
|
||||
|
||||
Info_Name = ConfigItem("Info", "Name", "")
|
||||
Info_Enabled = ConfigItem("Info", "Enabled", True, BoolValidator())
|
||||
|
||||
Data_Url = ConfigItem("Data", "Url", "", URLValidator())
|
||||
Data_Template = ConfigItem("Data", "Template", "")
|
||||
Data_Headers = ConfigItem("Data", "Headers", "{ }", JSONValidator())
|
||||
Data_Method = ConfigItem(
|
||||
"Data", "Method", "POST", OptionsValidator(["POST", "GET"])
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig(ConfigBase):
|
||||
"""全局配置"""
|
||||
|
||||
Function_HistoryRetentionTime = ConfigItem(
|
||||
"Function",
|
||||
"HistoryRetentionTime",
|
||||
0,
|
||||
OptionsValidator([7, 15, 30, 60, 90, 180, 365, 0]),
|
||||
)
|
||||
Function_IfAllowSleep = ConfigItem(
|
||||
"Function", "IfAllowSleep", False, BoolValidator()
|
||||
)
|
||||
Function_IfSilence = ConfigItem("Function", "IfSilence", False, BoolValidator())
|
||||
Function_BossKey = ConfigItem("Function", "BossKey", "")
|
||||
Function_IfAgreeBilibili = ConfigItem(
|
||||
"Function", "IfAgreeBilibili", False, BoolValidator()
|
||||
)
|
||||
Function_IfSkipMumuSplashAds = ConfigItem(
|
||||
"Function", "IfSkipMumuSplashAds", False, BoolValidator()
|
||||
)
|
||||
|
||||
Voice_Enabled = ConfigItem("Voice", "Enabled", False, BoolValidator())
|
||||
Voice_Type = ConfigItem(
|
||||
"Voice", "Type", "simple", OptionsValidator(["simple", "noisy"])
|
||||
)
|
||||
|
||||
Start_IfSelfStart = ConfigItem("Start", "IfSelfStart", False, BoolValidator())
|
||||
Start_IfMinimizeDirectly = ConfigItem(
|
||||
"Start", "IfMinimizeDirectly", False, BoolValidator()
|
||||
)
|
||||
|
||||
UI_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator())
|
||||
UI_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator())
|
||||
|
||||
Notify_SendTaskResultTime = ConfigItem(
|
||||
"Notify",
|
||||
"SendTaskResultTime",
|
||||
"不推送",
|
||||
OptionsValidator(["不推送", "任何时刻", "仅失败时"]),
|
||||
)
|
||||
Notify_IfSendStatistic = ConfigItem(
|
||||
"Notify", "IfSendStatistic", False, BoolValidator()
|
||||
)
|
||||
Notify_IfSendSixStar = ConfigItem("Notify", "IfSendSixStar", False, BoolValidator())
|
||||
Notify_IfPushPlyer = ConfigItem("Notify", "IfPushPlyer", False, BoolValidator())
|
||||
Notify_IfSendMail = ConfigItem("Notify", "IfSendMail", False, BoolValidator())
|
||||
Notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "")
|
||||
Notify_AuthorizationCode = ConfigItem(
|
||||
"Notify", "AuthorizationCode", "", EncryptValidator()
|
||||
)
|
||||
Notify_FromAddress = ConfigItem("Notify", "FromAddress", "")
|
||||
Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
|
||||
Notify_IfServerChan = ConfigItem("Notify", "IfServerChan", False, BoolValidator())
|
||||
Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
|
||||
Notify_CustomWebhooks = MultipleConfig([Webhook])
|
||||
|
||||
Update_IfAutoUpdate = ConfigItem("Update", "IfAutoUpdate", False, BoolValidator())
|
||||
Update_Source = ConfigItem(
|
||||
"Update",
|
||||
"Source",
|
||||
"GitHub",
|
||||
OptionsValidator(["GitHub", "MirrorChyan", "AutoSite"]),
|
||||
)
|
||||
Update_ProxyAddress = ConfigItem("Update", "ProxyAddress", "")
|
||||
Update_MirrorChyanCDK = ConfigItem(
|
||||
"Update", "MirrorChyanCDK", "", EncryptValidator()
|
||||
)
|
||||
|
||||
Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator())
|
||||
Data_LastStatisticsUpload = ConfigItem(
|
||||
"Data",
|
||||
"LastStatisticsUpload",
|
||||
"2000-01-01 00:00:00",
|
||||
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
Data_LastStageUpdated = ConfigItem(
|
||||
"Data",
|
||||
"LastStageUpdated",
|
||||
"2000-01-01 00:00:00",
|
||||
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
Data_StageTimeStamp = ConfigItem(
|
||||
"Data",
|
||||
"StageTimeStamp",
|
||||
"2000-01-01 00:00:00",
|
||||
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
Data_Stage = ConfigItem("Data", "Stage", "{ }", JSONValidator())
|
||||
Data_LastNoticeUpdated = ConfigItem(
|
||||
"Data",
|
||||
"LastNoticeUpdated",
|
||||
"2000-01-01 00:00:00",
|
||||
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
Data_IfShowNotice = ConfigItem("Data", "IfShowNotice", True, BoolValidator())
|
||||
Data_Notice = ConfigItem("Data", "Notice", "{ }", JSONValidator())
|
||||
Data_LastWebConfigUpdated = ConfigItem(
|
||||
"Data",
|
||||
"LastWebConfigUpdated",
|
||||
"2000-01-01 00:00:00",
|
||||
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
Data_WebConfig = ConfigItem("Data", "WebConfig", "{ }", JSONValidator())
|
||||
|
||||
|
||||
class QueueItem(ConfigBase):
|
||||
"""队列项配置"""
|
||||
|
||||
related_config: dict[str, MultipleConfig] = {}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_ScriptId = ConfigItem(
|
||||
"Info",
|
||||
"ScriptId",
|
||||
"-",
|
||||
MultipleUIDValidator("-", self.related_config, "ScriptConfig"),
|
||||
)
|
||||
|
||||
|
||||
class TimeSet(ConfigBase):
|
||||
"""时间设置配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Enabled = ConfigItem("Info", "Enabled", False, BoolValidator())
|
||||
self.Info_Time = ConfigItem("Info", "Time", "00:00", DateTimeValidator("%H:%M"))
|
||||
|
||||
|
||||
class QueueConfig(ConfigBase):
|
||||
"""队列配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新队列")
|
||||
self.Info_TimeEnabled = ConfigItem(
|
||||
"Info", "TimeEnabled", False, BoolValidator()
|
||||
)
|
||||
self.Info_StartUpEnabled = ConfigItem(
|
||||
"Info", "StartUpEnabled", False, BoolValidator()
|
||||
)
|
||||
self.Info_AfterAccomplish = ConfigItem(
|
||||
"Info",
|
||||
"AfterAccomplish",
|
||||
"NoAction",
|
||||
OptionsValidator(
|
||||
[
|
||||
"NoAction",
|
||||
"KillSelf",
|
||||
"Sleep",
|
||||
"Hibernate",
|
||||
"Shutdown",
|
||||
"ShutdownForce",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
self.Data_LastTimedStart = ConfigItem(
|
||||
"Data",
|
||||
"LastTimedStart",
|
||||
"2000-01-01 00:00",
|
||||
DateTimeValidator("%Y-%m-%d %H:%M"),
|
||||
)
|
||||
|
||||
self.TimeSet = MultipleConfig([TimeSet])
|
||||
self.QueueItem = MultipleConfig([QueueItem])
|
||||
|
||||
|
||||
class MaaUserConfig(ConfigBase):
|
||||
"""MAA用户配置"""
|
||||
|
||||
related_config: dict[str, MultipleConfig] = {}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator())
|
||||
self.Info_Id = ConfigItem("Info", "Id", "")
|
||||
self.Info_Mode = ConfigItem(
|
||||
"Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"])
|
||||
)
|
||||
self.Info_StageMode = ConfigItem(
|
||||
"Info",
|
||||
"StageMode",
|
||||
"Fixed",
|
||||
MultipleUIDValidator("Fixed", self.related_config, "PlanConfig"),
|
||||
)
|
||||
self.Info_Server = ConfigItem(
|
||||
"Info",
|
||||
"Server",
|
||||
"Official",
|
||||
OptionsValidator(
|
||||
["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"]
|
||||
),
|
||||
)
|
||||
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
|
||||
self.Info_RemainedDay = ConfigItem(
|
||||
"Info", "RemainedDay", -1, RangeValidator(-1, 9999)
|
||||
)
|
||||
self.Info_Annihilation = ConfigItem(
|
||||
"Info",
|
||||
"Annihilation",
|
||||
"Annihilation",
|
||||
OptionsValidator(
|
||||
[
|
||||
"Close",
|
||||
"Annihilation",
|
||||
"Chernobog@Annihilation",
|
||||
"LungmenOutskirts@Annihilation",
|
||||
"LungmenDowntown@Annihilation",
|
||||
]
|
||||
),
|
||||
)
|
||||
self.Info_Routine = ConfigItem("Info", "Routine", True, BoolValidator())
|
||||
self.Info_InfrastMode = ConfigItem(
|
||||
"Info",
|
||||
"InfrastMode",
|
||||
"Normal",
|
||||
OptionsValidator(["Normal", "Rotation", "Custom"]),
|
||||
)
|
||||
self.Info_InfrastPath = ConfigItem(
|
||||
"Info", "InfrastPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator())
|
||||
self.Info_Notes = ConfigItem("Info", "Notes", "无")
|
||||
self.Info_MedicineNumb = ConfigItem(
|
||||
"Info", "MedicineNumb", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.Info_SeriesNumb = ConfigItem(
|
||||
"Info",
|
||||
"SeriesNumb",
|
||||
"0",
|
||||
OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]),
|
||||
)
|
||||
self.Info_Stage = ConfigItem("Info", "Stage", "-")
|
||||
self.Info_Stage_1 = ConfigItem("Info", "Stage_1", "-")
|
||||
self.Info_Stage_2 = ConfigItem("Info", "Stage_2", "-")
|
||||
self.Info_Stage_3 = ConfigItem("Info", "Stage_3", "-")
|
||||
self.Info_Stage_Remain = ConfigItem("Info", "Stage_Remain", "-")
|
||||
self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator())
|
||||
self.Info_SklandToken = ConfigItem(
|
||||
"Info", "SklandToken", "", EncryptValidator()
|
||||
)
|
||||
|
||||
self.Data_LastProxyDate = ConfigItem(
|
||||
"Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
|
||||
)
|
||||
self.Data_LastAnnihilationDate = ConfigItem(
|
||||
"Data", "LastAnnihilationDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
|
||||
)
|
||||
self.Data_LastSklandDate = ConfigItem(
|
||||
"Data", "LastSklandDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
|
||||
)
|
||||
self.Data_ProxyTimes = ConfigItem(
|
||||
"Data", "ProxyTimes", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.Data_IfPassCheck = ConfigItem("Data", "IfPassCheck", True, BoolValidator())
|
||||
self.Data_CustomInfrastPlanIndex = ConfigItem(
|
||||
"Data", "CustomInfrastPlanIndex", "0"
|
||||
)
|
||||
|
||||
self.Task_IfWakeUp = ConfigItem("Task", "IfWakeUp", True, BoolValidator())
|
||||
self.Task_IfRecruiting = ConfigItem(
|
||||
"Task", "IfRecruiting", True, BoolValidator()
|
||||
)
|
||||
self.Task_IfBase = ConfigItem("Task", "IfBase", True, BoolValidator())
|
||||
self.Task_IfCombat = ConfigItem("Task", "IfCombat", True, BoolValidator())
|
||||
self.Task_IfMall = ConfigItem("Task", "IfMall", True, BoolValidator())
|
||||
self.Task_IfMission = ConfigItem("Task", "IfMission", True, BoolValidator())
|
||||
self.Task_IfAutoRoguelike = ConfigItem(
|
||||
"Task", "IfAutoRoguelike", False, BoolValidator()
|
||||
)
|
||||
self.Task_IfReclamation = ConfigItem(
|
||||
"Task", "IfReclamation", False, BoolValidator()
|
||||
)
|
||||
|
||||
self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator())
|
||||
self.Notify_IfSendStatistic = ConfigItem(
|
||||
"Notify", "IfSendStatistic", False, BoolValidator()
|
||||
)
|
||||
self.Notify_IfSendSixStar = ConfigItem(
|
||||
"Notify", "IfSendSixStar", False, BoolValidator()
|
||||
)
|
||||
self.Notify_IfSendMail = ConfigItem(
|
||||
"Notify", "IfSendMail", False, BoolValidator()
|
||||
)
|
||||
self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
|
||||
self.Notify_IfServerChan = ConfigItem(
|
||||
"Notify", "IfServerChan", False, BoolValidator()
|
||||
)
|
||||
self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
|
||||
self.Notify_CustomWebhooks = MultipleConfig([Webhook])
|
||||
|
||||
|
||||
class MaaConfig(ConfigBase):
|
||||
"""MAA配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新 MAA 脚本")
|
||||
self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator())
|
||||
|
||||
self.Run_TaskTransitionMethod = ConfigItem(
|
||||
"Run",
|
||||
"TaskTransitionMethod",
|
||||
"ExitEmulator",
|
||||
OptionsValidator(["NoAction", "ExitGame", "ExitEmulator"]),
|
||||
)
|
||||
self.Run_ProxyTimesLimit = ConfigItem(
|
||||
"Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.Run_ADBSearchRange = ConfigItem(
|
||||
"Run", "ADBSearchRange", 0, RangeValidator(0, 3)
|
||||
)
|
||||
self.Run_RunTimesLimit = ConfigItem(
|
||||
"Run", "RunTimesLimit", 3, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Run_AnnihilationTimeLimit = ConfigItem(
|
||||
"Run", "AnnihilationTimeLimit", 40, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Run_RoutineTimeLimit = ConfigItem(
|
||||
"Run", "RoutineTimeLimit", 10, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Run_AnnihilationWeeklyLimit = ConfigItem(
|
||||
"Run", "AnnihilationWeeklyLimit", True, BoolValidator()
|
||||
)
|
||||
|
||||
self.UserData = MultipleConfig([MaaUserConfig])
|
||||
|
||||
|
||||
class MaaPlanConfig(ConfigBase):
|
||||
"""MAA计划表配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新 MAA 计划表")
|
||||
self.Info_Mode = ConfigItem(
|
||||
"Info", "Mode", "ALL", OptionsValidator(["ALL", "Weekly"])
|
||||
)
|
||||
|
||||
self.config_item_dict: dict[str, Dict[str, ConfigItem]] = {}
|
||||
|
||||
for group in [
|
||||
"ALL",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
]:
|
||||
self.config_item_dict[group] = {}
|
||||
|
||||
self.config_item_dict[group]["MedicineNumb"] = ConfigItem(
|
||||
group, "MedicineNumb", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.config_item_dict[group]["SeriesNumb"] = ConfigItem(
|
||||
group,
|
||||
"SeriesNumb",
|
||||
"0",
|
||||
OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]),
|
||||
)
|
||||
self.config_item_dict[group]["Stage"] = ConfigItem(group, "Stage", "-")
|
||||
self.config_item_dict[group]["Stage_1"] = ConfigItem(group, "Stage_1", "-")
|
||||
self.config_item_dict[group]["Stage_2"] = ConfigItem(group, "Stage_2", "-")
|
||||
self.config_item_dict[group]["Stage_3"] = ConfigItem(group, "Stage_3", "-")
|
||||
self.config_item_dict[group]["Stage_Remain"] = ConfigItem(
|
||||
group, "Stage_Remain", "-"
|
||||
)
|
||||
|
||||
for name in [
|
||||
"MedicineNumb",
|
||||
"SeriesNumb",
|
||||
"Stage",
|
||||
"Stage_1",
|
||||
"Stage_2",
|
||||
"Stage_3",
|
||||
"Stage_Remain",
|
||||
]:
|
||||
setattr(self, f"{group}_{name}", self.config_item_dict[group][name])
|
||||
|
||||
def get_current_info(self, name: str) -> ConfigItem:
|
||||
"""获取当前的计划表配置项"""
|
||||
|
||||
if self.get("Info", "Mode") == "ALL":
|
||||
|
||||
return self.config_item_dict["ALL"][name]
|
||||
|
||||
elif self.get("Info", "Mode") == "Weekly":
|
||||
|
||||
dt = datetime.now()
|
||||
if dt.time() < datetime.min.time().replace(hour=4):
|
||||
dt = dt - timedelta(days=1)
|
||||
today = dt.strftime("%A")
|
||||
|
||||
if today in self.config_item_dict:
|
||||
return self.config_item_dict[today][name]
|
||||
else:
|
||||
return self.config_item_dict["ALL"][name]
|
||||
|
||||
else:
|
||||
raise ValueError("非法的计划表模式")
|
||||
|
||||
|
||||
class GeneralUserConfig(ConfigBase):
|
||||
"""通用脚本用户配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator())
|
||||
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
|
||||
self.Info_RemainedDay = ConfigItem(
|
||||
"Info", "RemainedDay", -1, RangeValidator(-1, 9999)
|
||||
)
|
||||
self.Info_IfScriptBeforeTask = ConfigItem(
|
||||
"Info", "IfScriptBeforeTask", False, BoolValidator()
|
||||
)
|
||||
self.Info_ScriptBeforeTask = ConfigItem(
|
||||
"Info", "ScriptBeforeTask", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Info_IfScriptAfterTask = ConfigItem(
|
||||
"Info", "IfScriptAfterTask", False, BoolValidator()
|
||||
)
|
||||
self.Info_ScriptAfterTask = ConfigItem(
|
||||
"Info", "ScriptAfterTask", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Info_Notes = ConfigItem("Info", "Notes", "无")
|
||||
|
||||
self.Data_LastProxyDate = ConfigItem(
|
||||
"Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
|
||||
)
|
||||
self.Data_ProxyTimes = ConfigItem(
|
||||
"Data", "ProxyTimes", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
|
||||
self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator())
|
||||
self.Notify_IfSendStatistic = ConfigItem(
|
||||
"Notify", "IfSendStatistic", False, BoolValidator()
|
||||
)
|
||||
self.Notify_IfSendMail = ConfigItem(
|
||||
"Notify", "IfSendMail", False, BoolValidator()
|
||||
)
|
||||
self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
|
||||
self.Notify_IfServerChan = ConfigItem(
|
||||
"Notify", "IfServerChan", False, BoolValidator()
|
||||
)
|
||||
self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
|
||||
self.Notify_CustomWebhooks = MultipleConfig([Webhook])
|
||||
|
||||
|
||||
class GeneralConfig(ConfigBase):
|
||||
"""通用配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新通用脚本")
|
||||
self.Info_RootPath = ConfigItem(
|
||||
"Info", "RootPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
|
||||
self.Script_ScriptPath = ConfigItem(
|
||||
"Script", "ScriptPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Script_Arguments = ConfigItem("Script", "Arguments", "")
|
||||
self.Script_IfTrackProcess = ConfigItem(
|
||||
"Script", "IfTrackProcess", False, BoolValidator()
|
||||
)
|
||||
self.Script_ConfigPath = ConfigItem(
|
||||
"Script", "ConfigPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Script_ConfigPathMode = ConfigItem(
|
||||
"Script", "ConfigPathMode", "File", OptionsValidator(["File", "Folder"])
|
||||
)
|
||||
self.Script_UpdateConfigMode = ConfigItem(
|
||||
"Script",
|
||||
"UpdateConfigMode",
|
||||
"Never",
|
||||
OptionsValidator(["Never", "Success", "Failure", "Always"]),
|
||||
)
|
||||
self.Script_LogPath = ConfigItem(
|
||||
"Script", "LogPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Script_LogPathFormat = ConfigItem("Script", "LogPathFormat", "%Y-%m-%d")
|
||||
self.Script_LogTimeStart = ConfigItem(
|
||||
"Script", "LogTimeStart", 1, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Script_LogTimeEnd = ConfigItem(
|
||||
"Script", "LogTimeEnd", 1, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Script_LogTimeFormat = ConfigItem(
|
||||
"Script", "LogTimeFormat", "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
self.Script_SuccessLog = ConfigItem("Script", "SuccessLog", "")
|
||||
self.Script_ErrorLog = ConfigItem("Script", "ErrorLog", "")
|
||||
|
||||
self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator())
|
||||
self.Game_Type = ConfigItem(
|
||||
"Game", "Type", "Emulator", OptionsValidator(["Emulator", "Client"])
|
||||
)
|
||||
self.Game_Path = ConfigItem("Game", "Path", str(Path.cwd()), FileValidator())
|
||||
self.Game_Arguments = ConfigItem("Game", "Arguments", "")
|
||||
self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999))
|
||||
self.Game_IfForceClose = ConfigItem(
|
||||
"Game", "IfForceClose", False, BoolValidator()
|
||||
)
|
||||
|
||||
self.Run_ProxyTimesLimit = ConfigItem(
|
||||
"Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.Run_RunTimesLimit = ConfigItem(
|
||||
"Run", "RunTimesLimit", 3, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Run_RunTimeLimit = ConfigItem(
|
||||
"Run", "RunTimeLimit", 10, RangeValidator(1, 9999)
|
||||
)
|
||||
|
||||
self.UserData = MultipleConfig([GeneralUserConfig])
|
||||
|
||||
|
||||
CLASS_BOOK = {"MAA": MaaConfig, "MaaPlan": MaaPlanConfig, "General": GeneralConfig}
|
||||
"""配置类映射表"""
|
||||
872
app/models/schema.py
Normal file
872
app/models/schema.py
Normal file
@@ -0,0 +1,872 @@
|
||||
# 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 WebhookIndexItem(BaseModel):
|
||||
uid: str = Field(..., description="唯一标识符")
|
||||
type: Literal["Webhook"] = Field(..., description="配置类型")
|
||||
|
||||
|
||||
class Webhook_Info(BaseModel):
|
||||
Name: Optional[str] = Field(default=None, description="Webhook名称")
|
||||
Enabled: Optional[bool] = Field(default=None, description="是否启用")
|
||||
|
||||
|
||||
class Webhook_Data(BaseModel):
|
||||
Url: Optional[str] = Field(default=None, description="Webhook URL")
|
||||
Template: Optional[str] = Field(default=None, description="消息模板")
|
||||
Headers: Optional[Dict[str, str]] = Field(default=None, description="自定义请求头")
|
||||
Method: Optional[Literal["POST", "GET"]] = Field(
|
||||
default=None, description="请求方法"
|
||||
)
|
||||
|
||||
|
||||
class Webhook(BaseModel):
|
||||
Info: Optional[Webhook_Info] = Field(default=None, description="Webhook基础信息")
|
||||
Data: Optional[Webhook_Data] = Field(default=None, description="Webhook配置数据")
|
||||
|
||||
|
||||
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 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推送密钥")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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 WebhookInBase(BaseModel):
|
||||
scriptId: Optional[str] = Field(
|
||||
default=None, description="所属脚本ID, 获取全局设置的Webhook数据时无需携带"
|
||||
)
|
||||
userId: Optional[str] = Field(
|
||||
default=None, description="所属用户ID, 获取全局设置的Webhook数据时无需携带"
|
||||
)
|
||||
|
||||
|
||||
class WebhookGetIn(WebhookInBase):
|
||||
webhookId: Optional[str] = Field(
|
||||
default=None, description="Webhook ID, 未携带时表示获取所有Webhook数据"
|
||||
)
|
||||
|
||||
|
||||
class WebhookGetOut(OutBase):
|
||||
index: List[WebhookIndexItem] = Field(..., description="Webhook索引列表")
|
||||
data: Dict[str, Webhook] = Field(
|
||||
..., description="Webhook数据字典, key来自于index列表的uid"
|
||||
)
|
||||
|
||||
|
||||
class WebhookCreateOut(OutBase):
|
||||
webhookId: str = Field(..., description="新创建的Webhook ID")
|
||||
data: Webhook = Field(..., description="Webhook配置数据")
|
||||
|
||||
|
||||
class WebhookUpdateIn(WebhookInBase):
|
||||
webhookId: str = Field(..., description="Webhook ID")
|
||||
data: Webhook = Field(..., description="Webhook更新数据")
|
||||
|
||||
|
||||
class WebhookDeleteIn(WebhookInBase):
|
||||
webhookId: str = Field(..., description="Webhook ID")
|
||||
|
||||
|
||||
class WebhookReorderIn(WebhookInBase):
|
||||
indexList: List[str] = Field(..., description="Webhook ID列表, 按新顺序排列")
|
||||
|
||||
|
||||
class WebhookTestIn(WebhookInBase):
|
||||
data: Webhook = Field(..., description="Webhook配置数据")
|
||||
|
||||
|
||||
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="版本更新信息字典")
|
||||
31
app/services/__init__.py
Normal file
31
app/services/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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
|
||||
|
||||
__version__ = "5.0.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .matomo import Matomo
|
||||
from .notification import Notify
|
||||
from .system import System
|
||||
from .update import Updater
|
||||
|
||||
__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()
|
||||
431
app/services/notification.py
Normal file
431
app/services/notification.py
Normal file
@@ -0,0 +1,431 @@
|
||||
# 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 json
|
||||
import smtplib
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from plyer import notification
|
||||
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 typing import Literal
|
||||
|
||||
from app.core import Config
|
||||
from app.models.config import Webhook
|
||||
from app.utils import get_logger, ImageUtils
|
||||
|
||||
logger = get_logger("通知服务")
|
||||
|
||||
|
||||
class Notification:
|
||||
|
||||
async def push_plyer(self, title: str, message: str, ticker: str, t: int) -> None:
|
||||
"""
|
||||
推送系统通知
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title: str
|
||||
通知标题
|
||||
message: str
|
||||
通知内容
|
||||
ticker: str
|
||||
通知横幅
|
||||
t: int
|
||||
通知持续时间
|
||||
"""
|
||||
|
||||
if not Config.get("Notify", "IfPushPlyer"):
|
||||
return
|
||||
|
||||
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 未正确导入, 无法推送系统通知")
|
||||
|
||||
async def send_mail(
|
||||
self, mode: Literal["文本", "网页"], title: str, content: str, to_address: str
|
||||
) -> None:
|
||||
"""
|
||||
推送邮件通知
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode: Literal["文本", "网页"]
|
||||
邮件内容模式, 支持 "文本" 和 "网页"
|
||||
title: str
|
||||
邮件标题
|
||||
content: str
|
||||
邮件内容
|
||||
to_address: str
|
||||
收件人地址
|
||||
"""
|
||||
|
||||
if Config.get("Notify", "SMTPServerAddress") == "":
|
||||
raise ValueError("邮件通知的SMTP服务器地址不能为空")
|
||||
if Config.get("Notify", "AuthorizationCode") == "":
|
||||
raise ValueError("邮件通知的授权码不能为空")
|
||||
if not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
Config.get("Notify", "FromAddress"),
|
||||
)
|
||||
):
|
||||
raise ValueError("邮件通知的发送邮箱格式错误或为空")
|
||||
if not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
to_address,
|
||||
)
|
||||
):
|
||||
raise ValueError("邮件通知的接收邮箱格式错误或为空")
|
||||
|
||||
# 定义邮件正文
|
||||
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: str, content: str, send_key: str) -> None:
|
||||
"""
|
||||
使用Server酱推送通知
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title: str
|
||||
通知标题
|
||||
content: str
|
||||
通知内容
|
||||
send_key: str
|
||||
Server酱的SendKey
|
||||
"""
|
||||
|
||||
if 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 WebhookPush(self, title: str, content: str, webhook: Webhook) -> None:
|
||||
"""
|
||||
Webhook 推送通知
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title: str
|
||||
通知标题
|
||||
content: str
|
||||
通知内容
|
||||
webhook: Webhook
|
||||
Webhook配置对象
|
||||
"""
|
||||
if not webhook.get("Info", "Enabled"):
|
||||
return
|
||||
|
||||
if webhook.get("Data", "Url") == "":
|
||||
raise ValueError("Webhook URL 不能为空")
|
||||
|
||||
# 解析模板
|
||||
template = (
|
||||
webhook.get("Data", "Template")
|
||||
or '{"title": "{title}", "content": "{content}"}'
|
||||
)
|
||||
|
||||
# 替换模板变量
|
||||
try:
|
||||
|
||||
# 准备模板变量
|
||||
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:
|
||||
# 解析模板为JSON对象,然后替换其中的变量
|
||||
template_obj = json.loads(template)
|
||||
|
||||
# 递归替换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"}
|
||||
headers.update(json.loads(webhook.get("Data", "Headers")))
|
||||
|
||||
if webhook.get("Data", "Method") == "POST":
|
||||
if isinstance(data, dict):
|
||||
response = requests.post(
|
||||
url=webhook.get("Data", "Url"),
|
||||
json=data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
elif isinstance(data, str):
|
||||
response = requests.post(
|
||||
url=webhook.get("Data", "Url"),
|
||||
data=data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
elif webhook.get("Data", "Method") == "GET":
|
||||
if isinstance(data, dict):
|
||||
# Flatten params to ensure all values are str or list of str
|
||||
params = {}
|
||||
for k, v in data.items():
|
||||
if isinstance(v, (dict, list)):
|
||||
params[k] = json.dumps(v, ensure_ascii=False)
|
||||
else:
|
||||
params[k] = str(v)
|
||||
else:
|
||||
params = {"message": str(data)}
|
||||
response = requests.get(
|
||||
url=webhook.get("Data", "Url"),
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
|
||||
# 检查响应
|
||||
if response.status_code == 200:
|
||||
logger.success(
|
||||
f"自定义Webhook推送成功: {webhook.get('Info', 'Name')} - {title}"
|
||||
)
|
||||
else:
|
||||
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
||||
|
||||
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通知
|
||||
for webhook in Config.Notify_CustomWebhooks.values():
|
||||
await self.WebhookPush(
|
||||
"AUTO-MAS测试通知",
|
||||
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
|
||||
webhook,
|
||||
)
|
||||
|
||||
logger.success("测试通知发送完成")
|
||||
|
||||
|
||||
Notify = Notification()
|
||||
383
app/services/system.py
Normal file
383
app/services/system.py
Normal file
@@ -0,0 +1,383 @@
|
||||
# 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 sys
|
||||
import ctypes
|
||||
import asyncio
|
||||
import win32gui
|
||||
import win32process
|
||||
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) -> None:
|
||||
self.power_task: Optional[asyncio.Task] = None
|
||||
|
||||
async def set_Sleep(self) -> None:
|
||||
"""同步系统休眠状态"""
|
||||
|
||||
if Config.get("Function", "IfAllowSleep"):
|
||||
# 设置系统电源状态
|
||||
ctypes.windll.kernel32.SetThreadExecutionState(
|
||||
self.ES_CONTINUOUS | self.ES_SYSTEM_REQUIRED
|
||||
)
|
||||
else:
|
||||
# 恢复系统电源状态
|
||||
ctypes.windll.kernel32.SetThreadExecutionState(self.ES_CONTINUOUS)
|
||||
|
||||
async def set_SelfStart(self) -> None:
|
||||
"""同步开机自启"""
|
||||
|
||||
if Config.get("Start", "IfSelfStart") and not await self.is_startup():
|
||||
|
||||
# 创建任务计划
|
||||
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 == "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("执行休眠操作")
|
||||
subprocess.run(["shutdown", "/h"])
|
||||
|
||||
elif mode == "Sleep":
|
||||
|
||||
logger.info("执行睡眠操作")
|
||||
subprocess.run(
|
||||
["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"]
|
||||
)
|
||||
|
||||
elif mode == "KillSelf" and Config.server is not None:
|
||||
|
||||
logger.info("执行退出主程序操作")
|
||||
Config.server.should_exit = True
|
||||
|
||||
elif sys.platform.startswith("linux"):
|
||||
|
||||
if mode == "NoAction":
|
||||
|
||||
logger.info("不执行系统电源操作")
|
||||
|
||||
elif mode == "Shutdown":
|
||||
|
||||
logger.info("执行关机操作")
|
||||
subprocess.run(["shutdown", "-h", "now"])
|
||||
|
||||
elif mode == "Hibernate":
|
||||
|
||||
logger.info("执行休眠操作")
|
||||
subprocess.run(["systemctl", "hibernate"])
|
||||
|
||||
elif mode == "Sleep":
|
||||
|
||||
logger.info("执行睡眠操作")
|
||||
subprocess.run(["systemctl", "suspend"])
|
||||
|
||||
elif mode == "KillSelf" and Config.server is not None:
|
||||
|
||||
logger.info("执行退出主程序操作")
|
||||
Config.server.should_exit = True
|
||||
|
||||
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:
|
||||
"""判断程序是否已经开机自启"""
|
||||
|
||||
try:
|
||||
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
|
||||
|
||||
async def get_window_info(self) -> list:
|
||||
"""获取当前前台窗口信息"""
|
||||
|
||||
def callback(hwnd, window_info):
|
||||
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
|
||||
_, pid = win32process.GetWindowThreadProcessId(hwnd)
|
||||
process = psutil.Process(pid)
|
||||
window_info.append((win32gui.GetWindowText(hwnd), process.exe()))
|
||||
return True
|
||||
|
||||
window_info = []
|
||||
win32gui.EnumWindows(callback, window_info)
|
||||
return window_info
|
||||
|
||||
async def kill_process(self, path: Path) -> None:
|
||||
"""
|
||||
根据路径中止进程
|
||||
|
||||
: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,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
killprocess.wait()
|
||||
|
||||
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"]):
|
||||
try:
|
||||
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
|
||||
|
||||
|
||||
System = _SystemHandler()
|
||||
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()
|
||||
2086
app/task/MAA.py
Normal file
2086
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"]
|
||||
1103
app/task/general.py
Normal file
1103
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}
|
||||
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()
|
||||
44
app/utils/__init__.py
Normal file
44
app/utils/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# 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 .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",
|
||||
]
|
||||
292
app/utils/constants.py
Normal file
292
app/utils/constants.py
Normal file
@@ -0,0 +1,292 @@
|
||||
# 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
|
||||
|
||||
|
||||
TYPE_BOOK = {"MaaConfig": "MAA", "GeneralConfig": "通用"}
|
||||
"""配置类型映射表"""
|
||||
|
||||
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"]
|
||||
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")
|
||||
@@ -1,5 +0,0 @@
|
||||
龙门币:CE-6
|
||||
技能:CA-5
|
||||
红票:AP-5
|
||||
经验:LS-6
|
||||
剿灭模式:Annihilation
|
||||
@@ -1,3 +0,0 @@
|
||||
管理密钥是解密用户密码的唯一凭证,与数据库绑定。密钥丢失或data/key/目录下任一文件损坏都将导致解密无法正常进行。
|
||||
|
||||
本项目采用自主开发的混合加密模式,项目组也无法找回您的管理密钥或修复data/key/目录下的文件。如果不幸的事发生,建议您删除data/data.db重新录入信息。
|
||||
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
|
||||
1404
frontend/electron/main.ts
Normal file
1404
frontend/electron/main.ts
Normal file
File diff suppressed because it is too large
Load Diff
83
frontend/electron/preload.ts
Normal file
83
frontend/electron/preload.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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),
|
||||
|
||||
// 对话框相关
|
||||
showQuestionDialog: (questionData: any) => ipcRenderer.invoke('show-question-dialog', questionData),
|
||||
dialogResponse: (messageId: string, choice: boolean) => ipcRenderer.invoke('dialog-response', messageId, choice),
|
||||
resizeDialogWindow: (height: number) => ipcRenderer.invoke('resize-dialog-window', height),
|
||||
moveWindow: (deltaX: number, deltaY: number) => ipcRenderer.invoke('move-window', deltaX, deltaY),
|
||||
|
||||
// 主题信息获取
|
||||
getThemeInfo: () => ipcRenderer.invoke('get-theme-info'),
|
||||
getTheme: () => ipcRenderer.invoke('get-theme'),
|
||||
|
||||
// 监听下载进度
|
||||
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,
|
||||
}
|
||||
}
|
||||
636
frontend/electron/services/gitService.ts
Normal file
636
frontend/electron/services/gitService.ts
Normal file
@@ -0,0 +1,636 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { spawn } from 'child_process'
|
||||
import { BrowserWindow, app } 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 getAppVersion(appRoot: string): string {
|
||||
console.log('=== 开始获取应用版本号 ===')
|
||||
console.log(`应用根目录: ${appRoot}`)
|
||||
|
||||
try {
|
||||
// 方法1: 从 Electron app 获取版本号(打包后可用)
|
||||
try {
|
||||
const appVersion = app.getVersion()
|
||||
if (appVersion && appVersion !== '1.0.0') { // 避免使用默认版本
|
||||
console.log(`✅ 从 app.getVersion() 获取版本号: ${appVersion}`)
|
||||
return appVersion
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ app.getVersion() 获取失败:', error)
|
||||
}
|
||||
|
||||
// 方法2: 从预设的环境变量获取(如果在构建时注入了)
|
||||
if (process.env.VITE_APP_VERSION) {
|
||||
console.log(`✅ 从环境变量获取版本号: ${process.env.VITE_APP_VERSION}`)
|
||||
return process.env.VITE_APP_VERSION
|
||||
}
|
||||
|
||||
// 方法3: 开发环境下从 package.json 获取
|
||||
const packageJsonPath = path.join(appRoot, 'frontend', 'package.json')
|
||||
console.log(`尝试读取前端package.json: ${packageJsonPath}`)
|
||||
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
||||
const version = packageJson.version || '获取版本失败!'
|
||||
console.log(`✅ 从前端package.json获取版本号: ${version}`)
|
||||
return version
|
||||
}
|
||||
|
||||
console.log('⚠️ 前端package.json不存在,尝试读取根目录package.json')
|
||||
|
||||
// 方法4: 从根目录 package.json 获取(开发环境)
|
||||
const currentPackageJsonPath = path.join(appRoot, 'package.json')
|
||||
console.log(`尝试读取根目录package.json: ${currentPackageJsonPath}`)
|
||||
|
||||
if (fs.existsSync(currentPackageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(currentPackageJsonPath, 'utf8'))
|
||||
const version = packageJson.version || '获取版本失败!'
|
||||
console.log(`✅ 从根目录package.json获取版本号: ${version}`)
|
||||
return version
|
||||
}
|
||||
|
||||
console.log('❌ 未找到任何版本信息源')
|
||||
return '获取版本失败!'
|
||||
} catch (error) {
|
||||
console.error('❌ 获取版本号失败:', error)
|
||||
return '获取版本失败!'
|
||||
}
|
||||
}
|
||||
|
||||
// 检查分支是否存在
|
||||
async function checkBranchExists(
|
||||
gitPath: string,
|
||||
gitEnv: any,
|
||||
repoUrl: string,
|
||||
branchName: string
|
||||
): Promise<boolean> {
|
||||
console.log(`=== 检查分支是否存在: ${branchName} ===`)
|
||||
console.log(`Git路径: ${gitPath}`)
|
||||
console.log(`仓库URL: ${repoUrl}`)
|
||||
|
||||
try {
|
||||
return new Promise<boolean>(resolve => {
|
||||
const proc = spawn(gitPath, ['ls-remote', '--heads', repoUrl, branchName], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
})
|
||||
|
||||
let output = ''
|
||||
let errorOutput = ''
|
||||
|
||||
proc.stdout?.on('data', data => {
|
||||
const chunk = data.toString()
|
||||
output += chunk
|
||||
console.log(`git ls-remote stdout: ${chunk.trim()}`)
|
||||
})
|
||||
|
||||
proc.stderr?.on('data', data => {
|
||||
const chunk = data.toString()
|
||||
errorOutput += chunk
|
||||
console.log(`git ls-remote stderr: ${chunk.trim()}`)
|
||||
})
|
||||
|
||||
proc.on('close', code => {
|
||||
console.log(`git ls-remote 退出码: ${code}`)
|
||||
// 如果输出包含分支名,说明分支存在
|
||||
const branchExists = output.includes(`refs/heads/${branchName}`)
|
||||
console.log(`分支 ${branchName} ${branchExists ? '✅ 存在' : '❌ 不存在'}`)
|
||||
if (errorOutput) {
|
||||
console.log(`错误输出: ${errorOutput}`)
|
||||
}
|
||||
resolve(branchExists)
|
||||
})
|
||||
|
||||
proc.on('error', error => {
|
||||
console.error(`git ls-remote 进程错误:`, error)
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`❌ 检查分支 ${branchName} 时出错:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 递归复制目录,包括文件和隐藏文件
|
||||
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
|
||||
}> {
|
||||
console.log('=== 开始克隆/更新后端代码 ===')
|
||||
console.log(`应用根目录: ${appRoot}`)
|
||||
console.log(`仓库URL: ${repoUrl}`)
|
||||
|
||||
try {
|
||||
const backendPath = appRoot
|
||||
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
|
||||
|
||||
console.log(`Git可执行文件路径: ${gitPath}`)
|
||||
console.log(`后端代码路径: ${backendPath}`)
|
||||
|
||||
if (!fs.existsSync(gitPath)) {
|
||||
const error = `Git可执行文件不存在: ${gitPath}`
|
||||
console.error(`❌ ${error}`)
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
console.log('✅ Git可执行文件存在')
|
||||
const gitEnv = getGitEnvironment(appRoot)
|
||||
console.log('✅ Git环境变量配置完成')
|
||||
|
||||
// 检查 git 是否可用
|
||||
console.log('=== 检查Git是否可用 ===')
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['--version'], { env: gitEnv })
|
||||
|
||||
proc.stdout?.on('data', data => {
|
||||
console.log(`git --version output: ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
proc.stderr?.on('data', data => {
|
||||
console.log(`git --version error: ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
proc.on('close', code => {
|
||||
console.log(`git --version 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log('✅ Git可用')
|
||||
resolve()
|
||||
} else {
|
||||
console.error('❌ Git无法正常运行')
|
||||
reject(new Error('git 无法正常运行'))
|
||||
}
|
||||
})
|
||||
|
||||
proc.on('error', error => {
|
||||
console.error('❌ Git进程启动失败:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// 获取版本号并确定目标分支
|
||||
const version = getAppVersion(appRoot)
|
||||
console.log(`=== 分支选择逻辑 ===`)
|
||||
console.log(`当前应用版本: ${version}`)
|
||||
|
||||
let targetBranch = 'feature/refactor' // 默认分支
|
||||
console.log(`默认分支: ${targetBranch}`)
|
||||
|
||||
if (version !== '获取版本失败!') {
|
||||
// 检查版本对应的分支是否存在
|
||||
console.log(`开始检查版本分支是否存在...`)
|
||||
const versionBranchExists = await checkBranchExists(gitPath, gitEnv, repoUrl, version)
|
||||
if (versionBranchExists) {
|
||||
targetBranch = version
|
||||
console.log(`🎯 将使用版本分支: ${targetBranch}`)
|
||||
} else {
|
||||
console.log(`⚠️ 版本分支 ${version} 不存在,使用默认分支: ${targetBranch}`)
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 版本号获取失败,使用默认分支: feature/refactor')
|
||||
}
|
||||
|
||||
console.log(`=== 最终选择分支: ${targetBranch} ===`)
|
||||
|
||||
// 检查是否为Git仓库
|
||||
const isRepo = isGitRepository(backendPath)
|
||||
console.log(`检查是否为Git仓库: ${isRepo ? '✅ 是' : '❌ 否'}`)
|
||||
|
||||
// ==== 下面是关键逻辑 ====
|
||||
if (isRepo) {
|
||||
console.log('=== 更新现有Git仓库 ===')
|
||||
|
||||
// 已是 git 仓库,先更新远程URL为镜像站,然后 pull
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: `正在更新后端代码(分支: ${targetBranch})...`,
|
||||
})
|
||||
}
|
||||
|
||||
// 更新远程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 stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git remote set-url stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git remote set-url 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log('✅ 远程URL更新成功')
|
||||
resolve()
|
||||
} else {
|
||||
console.error('❌ 远程URL更新失败')
|
||||
reject(new Error(`git remote set-url失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git remote set-url 进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// 获取目标分支信息(显式 fetch 目标分支)
|
||||
console.log(`📥 显式获取远程分支: ${targetBranch} ...`)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['fetch', 'origin', targetBranch], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: backendPath,
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git fetch stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git fetch stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git fetch origin ${targetBranch} 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log(`✅ 成功获取远程分支: ${targetBranch}`)
|
||||
resolve()
|
||||
} else {
|
||||
console.error(`❌ 获取远程分支失败: ${targetBranch}`)
|
||||
reject(new Error(`git fetch origin ${targetBranch} 失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git fetch 进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// 切换到目标分支
|
||||
console.log(`🔀 切换到目标分支: ${targetBranch}`)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['checkout', '-B', targetBranch, `origin/${targetBranch}`], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: backendPath,
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git checkout stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git checkout stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git checkout 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log(`✅ 成功切换到分支: ${targetBranch}`)
|
||||
resolve()
|
||||
} else {
|
||||
console.error(`❌ 切换分支失败: ${targetBranch}`)
|
||||
reject(new Error(`git checkout失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git checkout 进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// 执行pull操作
|
||||
console.log('🔄 强制同步到远程分支最新提交...')
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['reset', '--hard', `origin/${targetBranch}`], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: backendPath,
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git reset stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git reset stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git reset --hard 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log('✅ 代码已强制更新到远程最新版本')
|
||||
resolve()
|
||||
} else {
|
||||
console.error('❌ 代码重置失败')
|
||||
reject(new Error(`git reset --hard 失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git reset 进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: `后端代码更新完成(分支: ${targetBranch})`,
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`✅ 后端代码更新完成(分支: ${targetBranch})`)
|
||||
} else {
|
||||
console.log('=== 克隆新的Git仓库 ===')
|
||||
|
||||
// 不是 git 仓库,clone 到 tmp,再拷贝出来
|
||||
const tmpDir = path.join(appRoot, 'git_tmp')
|
||||
console.log(`临时目录: ${tmpDir}`)
|
||||
|
||||
if (fs.existsSync(tmpDir)) {
|
||||
console.log('🗑️ 清理现有临时目录...')
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
console.log('📁 创建临时目录...')
|
||||
fs.mkdirSync(tmpDir, { recursive: true })
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: `正在克隆后端代码(分支: ${targetBranch})...`,
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`📥 开始克隆代码到临时目录...`)
|
||||
console.log(`克隆参数: --single-branch --depth 1 --branch ${targetBranch}`)
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
gitPath,
|
||||
[
|
||||
'clone',
|
||||
'--progress',
|
||||
'--verbose',
|
||||
'--single-branch',
|
||||
'--depth',
|
||||
'1',
|
||||
'--branch',
|
||||
targetBranch,
|
||||
repoUrl,
|
||||
tmpDir,
|
||||
],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: appRoot,
|
||||
}
|
||||
)
|
||||
proc.stdout?.on('data', d => console.log('git clone stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git clone stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git clone 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log('✅ 代码克隆成功')
|
||||
resolve()
|
||||
} else {
|
||||
console.error('❌ 代码克隆失败')
|
||||
reject(new Error(`git clone失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git clone 进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// 复制所有文件到 backendPath(appRoot),包含 .git
|
||||
console.log('📋 复制文件到目标目录...')
|
||||
const tmpFiles = fs.readdirSync(tmpDir)
|
||||
console.log(`临时目录中的文件: ${tmpFiles.join(', ')}`)
|
||||
|
||||
for (const file of tmpFiles) {
|
||||
const src = path.join(tmpDir, file)
|
||||
const dst = path.join(backendPath, file)
|
||||
|
||||
console.log(`复制: ${file}`)
|
||||
|
||||
if (fs.existsSync(dst)) {
|
||||
console.log(` - 删除现有文件/目录: ${dst}`)
|
||||
if (fs.statSync(dst).isDirectory()) fs.rmSync(dst, { recursive: true, force: true })
|
||||
else fs.unlinkSync(dst)
|
||||
}
|
||||
|
||||
if (fs.statSync(src).isDirectory()) {
|
||||
console.log(` - 复制目录: ${src} -> ${dst}`)
|
||||
copyDirSync(src, dst)
|
||||
} else {
|
||||
console.log(` - 复制文件: ${src} -> ${dst}`)
|
||||
fs.copyFileSync(src, dst)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🗑️ 清理临时目录...')
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: `后端代码克隆完成(分支: ${targetBranch})`,
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`✅ 后端代码克隆完成(分支: ${targetBranch})`)
|
||||
}
|
||||
|
||||
console.log('=== 后端代码获取操作完成 ===')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('❌ 获取后端代码失败:', errorMessage)
|
||||
console.error('错误堆栈:', error instanceof Error ? error.stack : 'N/A')
|
||||
|
||||
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": "v5.0.0-alpha.3",
|
||||
"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 |
509
frontend/public/dialog.html
Normal file
509
frontend/public/dialog.html
Normal file
@@ -0,0 +1,509 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>操作确认</title>
|
||||
<style>
|
||||
:root {
|
||||
/* 亮色模式变量 */
|
||||
--primary-color: #1677ff;
|
||||
--primary-hover: #4096ff;
|
||||
--primary-active: #0958d9;
|
||||
--danger-color: #ff4d4f;
|
||||
--danger-hover: #ff7875;
|
||||
--danger-active: #d9363e;
|
||||
--success-color: #52c41a;
|
||||
--warning-color: #faad14;
|
||||
|
||||
--text-primary: #262626;
|
||||
--text-secondary: #595959;
|
||||
--text-tertiary: #8c8c8c;
|
||||
--text-disabled: #bfbfbf;
|
||||
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #fafafa;
|
||||
--bg-tertiary: #f5f5f5;
|
||||
--bg-quaternary: #f0f0f0;
|
||||
|
||||
--border-primary: #d9d9d9;
|
||||
--border-secondary: #f0f0f0;
|
||||
--border-hover: #4096ff;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
/* 暗色模式变量 */
|
||||
[data-theme="dark"] {
|
||||
--primary-color: #1668dc;
|
||||
--primary-hover: #3c89e8;
|
||||
--primary-active: #1554ad;
|
||||
--danger-color: #ff4d4f;
|
||||
--danger-hover: #ff7875;
|
||||
--danger-active: #d9363e;
|
||||
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #c9cdd4;
|
||||
--text-tertiary: #a6adb4;
|
||||
--text-disabled: #6c757d;
|
||||
|
||||
--bg-primary: #1f1f1f;
|
||||
--bg-secondary: #2a2a2a;
|
||||
--bg-tertiary: #373737;
|
||||
--bg-quaternary: #404040;
|
||||
|
||||
--border-primary: #434343;
|
||||
--border-secondary: #303030;
|
||||
--border-hover: #3c89e8;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 1px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px 0 rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.25), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
font-size: 14px;
|
||||
line-height: 1.5715;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: color 0.2s ease, background-color 0.2s ease;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-primary);
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
padding: 12px 16px 8px 16px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: move;
|
||||
-webkit-app-region: drag;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-primary);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-message {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
text-align: center;
|
||||
transition: color 0.2s ease;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: 12px 16px 12px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
padding: 6px 12px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
min-width: 60px;
|
||||
outline: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
line-height: 1.5715;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
background: var(--bg-quaternary);
|
||||
border-color: var(--border-hover);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.dialog-button:focus {
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2);
|
||||
border-color: var(--border-hover);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dialog-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
|
||||
.dialog-button.primary {
|
||||
background: var(--primary-color);
|
||||
color: #fff;
|
||||
border-color: var(--primary-color);
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dialog-button.primary:hover {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-button.primary:active {
|
||||
background: var(--primary-active);
|
||||
border-color: var(--primary-active);
|
||||
}
|
||||
|
||||
.dialog-button.danger {
|
||||
background: var(--danger-color);
|
||||
color: #fff;
|
||||
border-color: var(--danger-color);
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dialog-button.danger:hover {
|
||||
background: var(--danger-hover);
|
||||
border-color: var(--danger-hover);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-button.danger:active {
|
||||
background: var(--danger-active);
|
||||
border-color: var(--danger-active);
|
||||
}
|
||||
|
||||
/* 键盘导航样式 */
|
||||
.dialog-button:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.dialog-container {
|
||||
animation: dialogFadeIn 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
@keyframes dialogFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 350px) {
|
||||
.dialog-header {
|
||||
padding: 8px 12px 6px 12px;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.dialog-message {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: 8px 12px 8px 12px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="dialog-container" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-message">
|
||||
<div class="dialog-header">
|
||||
<h3 class="dialog-title" id="dialog-title">操作确认</h3>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<p class="dialog-message" id="dialog-message">是否要执行此操作?</p>
|
||||
</div>
|
||||
<div class="dialog-actions" id="dialog-actions" role="group" aria-label="对话框操作按钮">
|
||||
<!-- 按钮将通过 JavaScript 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 主题管理
|
||||
const ThemeManager = {
|
||||
init() {
|
||||
this.applyTheme();
|
||||
this.listenForThemeChanges();
|
||||
},
|
||||
|
||||
applyTheme() {
|
||||
// 优先从 Electron 主进程获取软件内主题状态
|
||||
if (window.electronAPI && window.electronAPI.getTheme) {
|
||||
window.electronAPI.getTheme().then(theme => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}).catch(() => {
|
||||
// 如果获取失败,使用系统主题
|
||||
this.useSystemTheme();
|
||||
});
|
||||
} else {
|
||||
this.useSystemTheme();
|
||||
}
|
||||
},
|
||||
|
||||
useSystemTheme() {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
||||
},
|
||||
|
||||
listenForThemeChanges() {
|
||||
// 监听系统主题变化
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
// 如果软件主题配置为系统跟随,则更新主题
|
||||
if (!window.electronAPI || !window.electronAPI.getTheme) {
|
||||
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
||||
} else {
|
||||
// 重新获取软件内主题状态
|
||||
this.applyTheme();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 Electron 主题变化
|
||||
if (window.electronAPI && window.electronAPI.onThemeChanged) {
|
||||
window.electronAPI.onThemeChanged((theme) => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 拖拽管理
|
||||
const DragManager = {
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
|
||||
init() {
|
||||
const header = document.querySelector('.dialog-header');
|
||||
if (!header) return;
|
||||
|
||||
header.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
||||
document.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
||||
|
||||
// 防止文本选择
|
||||
header.addEventListener('selectstart', (e) => e.preventDefault());
|
||||
},
|
||||
|
||||
handleMouseDown(e) {
|
||||
// 只有在头部区域才能拖拽
|
||||
if (!e.target.closest('.dialog-header')) return;
|
||||
|
||||
this.isDragging = true;
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
|
||||
const container = document.querySelector('.dialog-container');
|
||||
container.style.transition = 'none';
|
||||
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
handleMouseMove(e) {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const deltaX = e.clientX - this.startX;
|
||||
const deltaY = e.clientY - this.startY;
|
||||
|
||||
// 通知 Electron 移动窗口
|
||||
if (window.electronAPI && window.electronAPI.moveWindow) {
|
||||
window.electronAPI.moveWindow(deltaX, deltaY);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
handleMouseUp(e) {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
this.isDragging = false;
|
||||
|
||||
const container = document.querySelector('.dialog-container');
|
||||
container.style.transition = '';
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口加载完成后的初始化
|
||||
window.addEventListener('load', () => {
|
||||
// 初始化主题管理
|
||||
ThemeManager.init();
|
||||
});
|
||||
|
||||
// 获取传递的参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const data = JSON.parse(decodeURIComponent(urlParams.get('data') || '{}'));
|
||||
|
||||
// 设置对话框内容
|
||||
document.getElementById('dialog-title').textContent = data.title || '操作确认';
|
||||
document.getElementById('dialog-message').textContent = data.message || '是否要执行此操作?';
|
||||
|
||||
// 创建按钮
|
||||
const actionsContainer = document.getElementById('dialog-actions');
|
||||
const options = data.options || ['确定', '取消'];
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'dialog-button';
|
||||
button.textContent = option;
|
||||
|
||||
// 根据按钮文本设置样式
|
||||
if (option.includes('确定') || option.includes('是') || option.includes('同意')) {
|
||||
button.className += ' primary';
|
||||
} else if (option.includes('删除') || option.includes('危险')) {
|
||||
button.className += ' danger';
|
||||
}
|
||||
|
||||
// 绑定点击事件
|
||||
button.addEventListener('click', () => {
|
||||
// 添加点击动画
|
||||
button.style.transform = 'scale(0.98)';
|
||||
setTimeout(() => {
|
||||
button.style.transform = '';
|
||||
}, 100);
|
||||
|
||||
// 发送结果到主进程
|
||||
if (window.electronAPI && window.electronAPI.dialogResponse) {
|
||||
const choice = index === 0; // 第一个选项为 true
|
||||
window.electronAPI.dialogResponse(data.messageId, choice);
|
||||
}
|
||||
});
|
||||
|
||||
actionsContainer.appendChild(button);
|
||||
});
|
||||
|
||||
// 自动聚焦第一个按钮
|
||||
setTimeout(() => {
|
||||
const firstButton = actionsContainer.querySelector('.dialog-button');
|
||||
if (firstButton) {
|
||||
firstButton.focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 键盘事件处理
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
// ESC 键相当于取消
|
||||
if (window.electronAPI && window.electronAPI.dialogResponse) {
|
||||
window.electronAPI.dialogResponse(data.messageId, false);
|
||||
}
|
||||
} else if (event.key === 'Enter') {
|
||||
// Enter 键相当于确定
|
||||
const focusedButton = document.activeElement;
|
||||
if (focusedButton && focusedButton.classList.contains('dialog-button')) {
|
||||
focusedButton.click();
|
||||
} else {
|
||||
// 如果没有聚焦按钮,默认点击第一个
|
||||
const firstButton = actionsContainer.querySelector('.dialog-button');
|
||||
if (firstButton) {
|
||||
firstButton.click();
|
||||
}
|
||||
}
|
||||
} else if (event.key === 'Tab') {
|
||||
// Tab 键在按钮间切换
|
||||
const buttons = Array.from(actionsContainer.querySelectorAll('.dialog-button'));
|
||||
const currentIndex = buttons.indexOf(document.activeElement);
|
||||
|
||||
if (event.shiftKey) {
|
||||
// Shift+Tab 向前切换
|
||||
const prevIndex = currentIndex <= 0 ? buttons.length - 1 : currentIndex - 1;
|
||||
buttons[prevIndex].focus();
|
||||
} else {
|
||||
// Tab 向后切换
|
||||
const nextIndex = currentIndex >= buttons.length - 1 ? 0 : currentIndex + 1;
|
||||
buttons[nextIndex].focus();
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// 窗口加载完成后的初始化
|
||||
window.addEventListener('load', () => {
|
||||
// 初始化主题管理
|
||||
ThemeManager.init();
|
||||
|
||||
// 初始化拖拽管理
|
||||
DragManager.init();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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);
|
||||
};
|
||||
|
||||
22
frontend/src/api/models/GeneralUserConfig_Input.ts
Normal file
22
frontend/src/api/models/GeneralUserConfig_Input.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { GeneralUserConfig_Data } from './GeneralUserConfig_Data';
|
||||
import type { GeneralUserConfig_Info } from './GeneralUserConfig_Info';
|
||||
import type { UserConfig_Notify } from './UserConfig_Notify';
|
||||
export type GeneralUserConfig_Input = {
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
Info?: (GeneralUserConfig_Info | null);
|
||||
/**
|
||||
* 用户数据
|
||||
*/
|
||||
Data?: (GeneralUserConfig_Data | null);
|
||||
/**
|
||||
* 单独通知
|
||||
*/
|
||||
Notify?: (UserConfig_Notify | null);
|
||||
};
|
||||
|
||||
22
frontend/src/api/models/GeneralUserConfig_Output.ts
Normal file
22
frontend/src/api/models/GeneralUserConfig_Output.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { GeneralUserConfig_Data } from './GeneralUserConfig_Data';
|
||||
import type { GeneralUserConfig_Info } from './GeneralUserConfig_Info';
|
||||
import type { UserConfig_Notify } from './UserConfig_Notify';
|
||||
export type GeneralUserConfig_Output = {
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
Info?: (GeneralUserConfig_Info | null);
|
||||
/**
|
||||
* 用户数据
|
||||
*/
|
||||
Data?: (GeneralUserConfig_Data | null);
|
||||
/**
|
||||
* 单独通知
|
||||
*/
|
||||
Notify?: (UserConfig_Notify | null);
|
||||
};
|
||||
|
||||
27
frontend/src/api/models/GetStageIn.ts
Normal file
27
frontend/src/api/models/GetStageIn.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GetStageIn = {
|
||||
/**
|
||||
* 选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项
|
||||
*/
|
||||
type: GetStageIn.type;
|
||||
};
|
||||
export namespace GetStageIn {
|
||||
/**
|
||||
* 选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项
|
||||
*/
|
||||
export enum type {
|
||||
TODAY = 'Today',
|
||||
ALL = 'ALL',
|
||||
MONDAY = 'Monday',
|
||||
TUESDAY = 'Tuesday',
|
||||
WEDNESDAY = 'Wednesday',
|
||||
THURSDAY = 'Thursday',
|
||||
FRIDAY = 'Friday',
|
||||
SATURDAY = 'Saturday',
|
||||
SUNDAY = 'Sunday',
|
||||
}
|
||||
}
|
||||
|
||||
31
frontend/src/api/models/GlobalConfig_Function.ts
Normal file
31
frontend/src/api/models/GlobalConfig_Function.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GlobalConfig_Function = {
|
||||
/**
|
||||
* 历史记录保留时间, 0表示永久保存
|
||||
*/
|
||||
HistoryRetentionTime?: (7 | 15 | 30 | 60 | 90 | 180 | 365 | 0 | null);
|
||||
/**
|
||||
* 允许休眠
|
||||
*/
|
||||
IfAllowSleep?: (boolean | null);
|
||||
/**
|
||||
* 静默模式
|
||||
*/
|
||||
IfSilence?: (boolean | null);
|
||||
/**
|
||||
* 模拟器老板键
|
||||
*/
|
||||
BossKey?: (string | null);
|
||||
/**
|
||||
* 同意哔哩哔哩用户协议
|
||||
*/
|
||||
IfAgreeBilibili?: (boolean | null);
|
||||
/**
|
||||
* 跳过Mumu模拟器启动广告
|
||||
*/
|
||||
IfSkipMumuSplashAds?: (boolean | null);
|
||||
};
|
||||
|
||||
37
frontend/src/api/models/GlobalConfig_Input.ts
Normal file
37
frontend/src/api/models/GlobalConfig_Input.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { GlobalConfig_Function } from './GlobalConfig_Function';
|
||||
import type { GlobalConfig_Notify } from './GlobalConfig_Notify';
|
||||
import type { GlobalConfig_Start } from './GlobalConfig_Start';
|
||||
import type { GlobalConfig_UI } from './GlobalConfig_UI';
|
||||
import type { GlobalConfig_Update } from './GlobalConfig_Update';
|
||||
import type { GlobalConfig_Voice } from './GlobalConfig_Voice';
|
||||
export type GlobalConfig_Input = {
|
||||
/**
|
||||
* 功能相关配置
|
||||
*/
|
||||
Function?: (GlobalConfig_Function | null);
|
||||
/**
|
||||
* 语音相关配置
|
||||
*/
|
||||
Voice?: (GlobalConfig_Voice | null);
|
||||
/**
|
||||
* 启动相关配置
|
||||
*/
|
||||
Start?: (GlobalConfig_Start | null);
|
||||
/**
|
||||
* 界面相关配置
|
||||
*/
|
||||
UI?: (GlobalConfig_UI | null);
|
||||
/**
|
||||
* 通知相关配置
|
||||
*/
|
||||
Notify?: (GlobalConfig_Notify | null);
|
||||
/**
|
||||
* 更新相关配置
|
||||
*/
|
||||
Update?: (GlobalConfig_Update | null);
|
||||
};
|
||||
|
||||
56
frontend/src/api/models/GlobalConfig_Notify.ts
Normal file
56
frontend/src/api/models/GlobalConfig_Notify.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { CustomWebhook } from './CustomWebhook';
|
||||
export type GlobalConfig_Notify = {
|
||||
/**
|
||||
* 任务结果推送时机
|
||||
*/
|
||||
SendTaskResultTime?: ('不推送' | '任何时刻' | '仅失败时' | null);
|
||||
/**
|
||||
* 是否发送统计信息
|
||||
*/
|
||||
IfSendStatistic?: (boolean | null);
|
||||
/**
|
||||
* 是否发送公招六星通知
|
||||
*/
|
||||
IfSendSixStar?: (boolean | null);
|
||||
/**
|
||||
* 是否推送系统通知
|
||||
*/
|
||||
IfPushPlyer?: (boolean | null);
|
||||
/**
|
||||
* 是否发送邮件通知
|
||||
*/
|
||||
IfSendMail?: (boolean | null);
|
||||
/**
|
||||
* SMTP服务器地址
|
||||
*/
|
||||
SMTPServerAddress?: (string | null);
|
||||
/**
|
||||
* SMTP授权码
|
||||
*/
|
||||
AuthorizationCode?: (string | null);
|
||||
/**
|
||||
* 邮件发送地址
|
||||
*/
|
||||
FromAddress?: (string | null);
|
||||
/**
|
||||
* 邮件接收地址
|
||||
*/
|
||||
ToAddress?: (string | null);
|
||||
/**
|
||||
* 是否使用ServerChan推送
|
||||
*/
|
||||
IfServerChan?: (boolean | null);
|
||||
/**
|
||||
* ServerChan推送密钥
|
||||
*/
|
||||
ServerChanKey?: (string | null);
|
||||
/**
|
||||
* 自定义Webhook列表
|
||||
*/
|
||||
CustomWebhooks?: (Array<CustomWebhook> | null);
|
||||
};
|
||||
|
||||
37
frontend/src/api/models/GlobalConfig_Output.ts
Normal file
37
frontend/src/api/models/GlobalConfig_Output.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { GlobalConfig_Function } from './GlobalConfig_Function';
|
||||
import type { GlobalConfig_Notify } from './GlobalConfig_Notify';
|
||||
import type { GlobalConfig_Start } from './GlobalConfig_Start';
|
||||
import type { GlobalConfig_UI } from './GlobalConfig_UI';
|
||||
import type { GlobalConfig_Update } from './GlobalConfig_Update';
|
||||
import type { GlobalConfig_Voice } from './GlobalConfig_Voice';
|
||||
export type GlobalConfig_Output = {
|
||||
/**
|
||||
* 功能相关配置
|
||||
*/
|
||||
Function?: (GlobalConfig_Function | null);
|
||||
/**
|
||||
* 语音相关配置
|
||||
*/
|
||||
Voice?: (GlobalConfig_Voice | null);
|
||||
/**
|
||||
* 启动相关配置
|
||||
*/
|
||||
Start?: (GlobalConfig_Start | null);
|
||||
/**
|
||||
* 界面相关配置
|
||||
*/
|
||||
UI?: (GlobalConfig_UI | null);
|
||||
/**
|
||||
* 通知相关配置
|
||||
*/
|
||||
Notify?: (GlobalConfig_Notify | null);
|
||||
/**
|
||||
* 更新相关配置
|
||||
*/
|
||||
Update?: (GlobalConfig_Update | null);
|
||||
};
|
||||
|
||||
15
frontend/src/api/models/GlobalConfig_Start.ts
Normal file
15
frontend/src/api/models/GlobalConfig_Start.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GlobalConfig_Start = {
|
||||
/**
|
||||
* 是否在系统启动时自动运行
|
||||
*/
|
||||
IfSelfStart?: (boolean | null);
|
||||
/**
|
||||
* 启动时是否直接最小化到托盘而不显示主窗口
|
||||
*/
|
||||
IfMinimizeDirectly?: (boolean | null);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user