mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 03:10:48 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
924dfd6a6f | ||
|
|
07f43538df | ||
|
|
4fa5fa2014 | ||
|
|
f9b9543a6d | ||
|
|
af97543918 | ||
|
|
a508522872 | ||
|
|
f1ca2134b7 | ||
|
|
a9755655b2 | ||
|
|
6a76e82f26 | ||
|
|
6775f5d123 | ||
|
|
ad6e08525c | ||
|
|
51ef4e7476 | ||
|
|
e5b35273f9 | ||
|
|
647ed3b66c | ||
|
|
4667af4ebe | ||
|
|
3f775a0e6c | ||
|
|
fa79de3f05 | ||
|
|
ffa8b25a66 | ||
|
|
05f8bbd26e | ||
|
|
7d43b14f81 | ||
|
|
cecc5aeb15 | ||
|
|
43d6ac2071 | ||
|
|
678e3e2dc6 | ||
|
|
75f91e1473 | ||
|
|
bdc45c538c | ||
|
|
955c7377d7 | ||
|
|
c71abd8c86 | ||
|
|
ab27a4c908 | ||
|
|
026640a757 | ||
|
|
a1501705c1 | ||
|
|
b321530dcf | ||
|
|
02c6ad8004 | ||
|
|
fff572869e | ||
|
|
b733937691 | ||
|
|
0d9eba4b6e | ||
|
|
20c6a1e8bf | ||
|
|
56bc2f0b1d | ||
|
|
ecc55883f0 | ||
|
|
185fd2ff91 | ||
|
|
3822c37fa0 | ||
|
|
4244502949 | ||
|
|
71dd82f74e | ||
|
|
6ea8d361fd | ||
|
|
3f71f9acdb | ||
|
|
418695c4aa | ||
|
|
4c5223ba11 | ||
|
|
2f70e10be7 | ||
|
|
66706ce150 | ||
|
|
3d719baa54 | ||
|
|
926ad067c1 | ||
|
|
ff98c3a499 | ||
|
|
69fcf45231 | ||
|
|
c3704502bf | ||
|
|
a0613d0f39 | ||
|
|
8c0f46420e | ||
|
|
ea4b474b83 | ||
|
|
53ef0fe296 | ||
|
|
c573186e34 | ||
|
|
c229eca315 | ||
|
|
a41dd74048 | ||
|
|
459f465f3b | ||
|
|
b508e97dc1 | ||
|
|
7414a9d33d | ||
|
|
a48d100b2d | ||
|
|
a3044f9d29 | ||
|
|
19cfc9e351 | ||
|
|
6c61ac5106 | ||
|
|
65f7483688 | ||
|
|
8b042d90f3 | ||
|
|
4e5ad6d5f3 | ||
|
|
8b8eae211b | ||
|
|
f4cb3eacf8 | ||
|
|
f6021c8157 | ||
|
|
73601ca853 | ||
|
|
8218d4ba94 | ||
|
|
d47f835531 | ||
|
|
230a8f61b8 | ||
|
|
af5de8372c | ||
|
|
a743aa8115 | ||
|
|
fe4516df00 | ||
|
|
be2da98674 | ||
|
|
0da8155bac | ||
|
|
eddd258614 | ||
|
|
2ad6437672 | ||
|
|
162bd9d8cc | ||
|
|
e694570f1a | ||
|
|
eb5cc3943d | ||
|
|
1b39d3ab48 | ||
|
|
f47a788184 | ||
|
|
738fe04ce4 | ||
|
|
a2812d9c03 | ||
|
|
5c53ae4993 | ||
|
|
e191aca41f | ||
|
|
5afac3a3ac | ||
|
|
71ba1534f2 | ||
|
|
791cf657b5 | ||
|
|
c1ef021009 | ||
|
|
90fe63211d | ||
|
|
b60cda66bb | ||
|
|
7efe81df9e | ||
|
|
0d492e94f4 | ||
|
|
a45efbd69b | ||
|
|
035f0eb9e3 | ||
|
|
7d0cae7839 | ||
|
|
46e71d8213 | ||
|
|
372afb92c6 | ||
|
|
69d9307be9 | ||
|
|
c3908e956f | ||
|
|
1e749f3731 | ||
|
|
a68f4b0cde | ||
|
|
07541569c5 | ||
|
|
70f2bef5d2 | ||
|
|
805a716033 | ||
|
|
e77d4c4337 | ||
|
|
b309ac7613 | ||
|
|
8dd379b6b9 | ||
|
|
0b52e05af8 | ||
|
|
e9852a0002 | ||
|
|
20be84b352 | ||
|
|
4ab24ea1b9 | ||
|
|
417f91207f | ||
|
|
f723631647 | ||
|
|
6657ff0560 | ||
|
|
0593dae41d | ||
|
|
445dee00f7 | ||
|
|
6ca28190b0 | ||
|
|
510534ee0c | ||
|
|
2e7f3a1578 | ||
|
|
591a17e7ee | ||
|
|
bd7c361300 | ||
|
|
409b3281cc | ||
|
|
ac6de4911a | ||
|
|
1628e05ece | ||
|
|
7b7e03b503 | ||
|
|
8d14c9dae1 | ||
|
|
4d6d0a55e7 | ||
|
|
8ab2dc3280 | ||
|
|
3f54048127 | ||
|
|
47c2c61f33 | ||
|
|
47d2fc24bc | ||
|
|
7a8a8ab9b3 | ||
|
|
2ac4581d54 | ||
|
|
68ad01e0e8 | ||
|
|
cdeca4b808 | ||
|
|
55945e4cd1 | ||
|
|
b39036f8f8 | ||
|
|
f07093d29f | ||
|
|
11c789c71c | ||
|
|
81898f9dd7 | ||
|
|
6dba8ae72d | ||
|
|
c47896e787 | ||
|
|
14cb1f7788 | ||
|
|
892d91edc4 | ||
|
|
74916670eb | ||
|
|
b31eab02d7 | ||
|
|
9c258ab113 | ||
|
|
3bcba11b33 | ||
|
|
15140fd87d | ||
|
|
e83058c3bc | ||
|
|
29f0c2c9ac | ||
|
|
f25fe27473 | ||
|
|
0ae1f284ec | ||
|
|
49c441117a | ||
|
|
7bc5ff01a6 | ||
|
|
8128fb8a9f | ||
|
|
315ae49360 | ||
|
|
35c6475534 | ||
|
|
7f3e60423b | ||
|
|
0a723318a1 | ||
|
|
2c1e7535b8 | ||
|
|
48d1fba699 | ||
|
|
06f861baaf | ||
|
|
bbbd353567 | ||
|
|
8283f62db2 | ||
|
|
1a94a660a5 | ||
|
|
5c393fef36 | ||
|
|
53346a3962 | ||
|
|
4e66a3aff5 | ||
|
|
b79d10b452 | ||
|
|
3dc3ece993 | ||
|
|
792cf9ad20 | ||
|
|
201f41e15b | ||
|
|
71e7ba9cb6 | ||
|
|
a7562259d1 | ||
|
|
18671264bf | ||
|
|
37bcb77be0 | ||
|
|
563457cf1b | ||
|
|
d2cf482d11 | ||
|
|
2713942a68 | ||
|
|
62168979e3 | ||
|
|
f8aba4b1fb | ||
|
|
52a89a21f6 | ||
|
|
ca8790bbec | ||
|
|
be1d68c0d1 | ||
|
|
95afcbc9dd | ||
|
|
20249119a0 | ||
|
|
6e0ee6f627 | ||
|
|
13e7dac333 | ||
|
|
b89dc6dfc6 | ||
|
|
897b710527 | ||
|
|
5afec03548 | ||
|
|
c6a25e5359 | ||
|
|
3777a4e7b4 | ||
|
|
9b7d4cb8fe | ||
|
|
59f7b72bb1 | ||
|
|
4d97f0f44d | ||
|
|
de9e9ce627 | ||
|
|
f056b611a4 | ||
|
|
659484511f | ||
|
|
9ed107980c | ||
|
|
4a3ac7dc4b | ||
|
|
c3a0f58003 | ||
|
|
23781d8b34 | ||
|
|
c1d6a0a40b | ||
|
|
82c9978514 | ||
|
|
9f6d4943ea | ||
|
|
c4a44a48c5 | ||
|
|
d2fef75e01 | ||
|
|
085cb789b5 | ||
|
|
20f615eadf | ||
|
|
66a2769e7c | ||
|
|
899a30eacd | ||
|
|
8760ac2964 | ||
|
|
62a29867a3 | ||
|
|
4938635ef4 | ||
|
|
046239404f | ||
|
|
b90243f55c | ||
|
|
43c0cd8132 | ||
|
|
533e12a796 | ||
|
|
bd004a0489 | ||
|
|
41befcc0a8 | ||
|
|
9fb9b3d121 | ||
|
|
5055045d22 | ||
|
|
9f7beb934d | ||
|
|
26b06c17c5 | ||
|
|
b8a9b26ca3 | ||
|
|
b6376ed2e8 | ||
|
|
99f6073c8e | ||
|
|
5e73f0390f | ||
|
|
c35cfba8b0 | ||
|
|
aac109621a | ||
|
|
a340964c82 | ||
|
|
00818b694a | ||
|
|
ec5b885476 | ||
|
|
9d55cb108c | ||
|
|
4c19054b30 | ||
|
|
2a41f64d1b | ||
|
|
d3023d31e3 | ||
|
|
eddc03e295 | ||
|
|
77a8e00b17 | ||
|
|
99281a061e | ||
|
|
22eedebf14 | ||
|
|
d7e0531935 | ||
|
|
16734326cf | ||
|
|
956cfe18be | ||
|
|
a864a56917 | ||
|
|
ab3fc9b5f1 | ||
|
|
8d899f1a53 | ||
|
|
a1eef2c644 | ||
|
|
aab096843c | ||
|
|
acc7443004 | ||
|
|
042d7d3b8e | ||
|
|
0511a4af0d | ||
|
|
748a099087 | ||
|
|
742d0b2135 | ||
|
|
2081364325 | ||
|
|
39dc31916e | ||
|
|
5395cebdb4 | ||
|
|
fe934fa4af | ||
|
|
461fa8e131 | ||
|
|
4618a19c04 | ||
|
|
e2bd15683e | ||
|
|
648b9c4520 | ||
|
|
b4e4e84bbd | ||
|
|
aa5d6f3cb6 | ||
|
|
e9416c23df | ||
|
|
ef4999be55 | ||
|
|
e7be4999b0 | ||
|
|
5907553670 | ||
|
|
0b9066525a | ||
|
|
ac904cae61 | ||
|
|
4d5fa320ea | ||
|
|
003defedcf | ||
|
|
40e8041401 | ||
|
|
a8174e89be | ||
|
|
cce83f7a4c | ||
|
|
4f7649f157 | ||
|
|
b002cbef3a | ||
|
|
1d93fe0de0 | ||
|
|
2d936f1341 | ||
|
|
b40673e9ac | ||
|
|
637874e426 | ||
|
|
de71d1a258 | ||
|
|
e174e190ee | ||
|
|
89164a3bd2 | ||
|
|
a9b9c4d7ad | ||
|
|
f947866c32 | ||
|
|
a2e33d1ed4 | ||
|
|
b1a242c8ee | ||
|
|
0bf28e9ab3 | ||
|
|
624c9374e5 | ||
|
|
a9da8fffc1 | ||
|
|
b4e5db9da9 | ||
|
|
2e27d4057e | ||
|
|
e7d023a7e0 | ||
|
|
39bfd39559 | ||
|
|
6832cb4e22 | ||
|
|
590dbba19b | ||
|
|
3987ef8d97 | ||
|
|
8b0dedaf8c | ||
|
|
04edd97967 | ||
|
|
6131fe0110 | ||
|
|
665c344ae4 | ||
|
|
373b8c97d7 | ||
|
|
56eb0c5408 | ||
|
|
fe55929959 | ||
|
|
01e2bd007d | ||
|
|
4e091b0c3e | ||
|
|
db94cd3799 | ||
|
|
a15192b9b2 | ||
|
|
efd4f1d5ab | ||
|
|
bd166ee794 | ||
|
|
93e94ea94c | ||
|
|
88b87de391 | ||
|
|
8affff96f3 | ||
|
|
936b565fb1 | ||
|
|
b69ff2895e | ||
|
|
f3791cbb62 | ||
|
|
b0f3b51736 | ||
|
|
3aa112e373 | ||
|
|
683482e067 | ||
|
|
5a2caeb66b | ||
|
|
ee6e5bd8b4 | ||
|
|
f970322f0a | ||
|
|
1718120623 | ||
|
|
702c3160ec | ||
|
|
db227e56bf | ||
|
|
84828865f7 | ||
|
|
87455cebc0 | ||
|
|
9f4435b237 | ||
|
|
e5bc4b3b04 | ||
|
|
11e1b50000 | ||
|
|
bec226ae13 | ||
|
|
c9ecdc6c97 | ||
|
|
4cf64e7af0 | ||
|
|
4ce01c5f0e | ||
|
|
2eff99abe1 | ||
|
|
5bf57185d9 | ||
|
|
c59dac19ae | ||
|
|
f1ef8e72ae | ||
|
|
5b1accffb7 | ||
|
|
f64ff83414 | ||
|
|
26c914d1db | ||
|
|
8f75a54110 | ||
|
|
874b6e3a56 | ||
|
|
f305d45225 | ||
|
|
a7f97bd729 | ||
|
|
0bbb7c7b89 | ||
|
|
f7465759c7 | ||
|
|
998e82d323 | ||
|
|
f8dfee8945 | ||
|
|
86e3d8736b | ||
|
|
85d32d00e1 | ||
|
|
6a52e2f804 | ||
|
|
b4cb72c5d0 | ||
|
|
79fd4ec58f | ||
|
|
49ed5c6632 | ||
|
|
ffba7ee4a1 | ||
|
|
91d080472f | ||
|
|
826f214f0f | ||
|
|
ca150dc6a6 | ||
|
|
ae5db60211 | ||
|
|
490bdc15f6 | ||
|
|
1377fe31aa | ||
|
|
3d1cf6bf2e | ||
|
|
6ac755b478 | ||
|
|
e70fa83074 | ||
|
|
41521f1d08 | ||
|
|
8b7bef097c | ||
|
|
618756d4a9 | ||
|
|
70d0c71414 | ||
|
|
65eb9c806d | ||
|
|
50718ba714 | ||
|
|
7c3bb90d12 | ||
|
|
f66390eabd | ||
|
|
481646501c | ||
|
|
5baa8e4391 | ||
|
|
3d11210aeb | ||
|
|
e1863407f2 | ||
|
|
0c28b63ab7 | ||
|
|
8b8bd279c6 | ||
|
|
01b404765e | ||
|
|
c231a871d7 | ||
|
|
598c6b4e57 | ||
|
|
2295fabcc5 | ||
|
|
be94d796ac | ||
|
|
c84908d7fa | ||
|
|
d8ae039b92 | ||
|
|
bce1431d03 | ||
|
|
9db0095e29 | ||
|
|
77dc7817fb | ||
|
|
d0dd97631e | ||
|
|
d11d6d0c18 | ||
|
|
0af687f781 | ||
|
|
8db997abe8 | ||
|
|
aab6bbeb15 | ||
|
|
df1addc1ff | ||
|
|
1f8f35476a | ||
|
|
5354fc76db | ||
|
|
b29a9e012c | ||
|
|
6e5d89c197 | ||
|
|
d6cfb18f06 | ||
|
|
9a3e38051d | ||
|
|
b705ad6ee8 | ||
|
|
db564d1add | ||
|
|
4f422c0658 | ||
|
|
f267720af4 | ||
|
|
4a0c66bcc0 | ||
|
|
ef7283a9fd | ||
|
|
4498449eea | ||
|
|
ebb9676a51 | ||
|
|
6fb39ce835 | ||
|
|
59c26d90d3 | ||
|
|
1d2df860e8 | ||
|
|
ce22cad5b2 | ||
|
|
dc44ce6b1a | ||
|
|
b27ee23cc3 | ||
|
|
083c8869aa | ||
|
|
6a971a0d6e | ||
|
|
a25bfb6912 | ||
|
|
f19dd21155 | ||
|
|
79b342d1d2 | ||
|
|
20b0d75b0d | ||
|
|
c89724906e | ||
|
|
c3a548ece3 | ||
|
|
b431e39f8f | ||
|
|
710746809b | ||
|
|
8a225e9beb | ||
|
|
a5ecb204b2 | ||
|
|
f1f009da3b | ||
|
|
7291098f98 | ||
|
|
666545ff03 | ||
|
|
81e2bbb14c | ||
|
|
970dff144c | ||
|
|
819b15b15e | ||
|
|
3fa57c69ac | ||
|
|
18689c8119 | ||
|
|
61e7049baa | ||
|
|
39c40a328d | ||
|
|
955d815d14 | ||
|
|
3a3b945de8 | ||
|
|
73c53dbc8d | ||
|
|
f76828d4ec | ||
|
|
7b2956eb0a | ||
|
|
b2167ae5e4 | ||
|
|
98dfa4c425 | ||
|
|
5bf08b76de | ||
|
|
5c5b4f804e | ||
|
|
6455245d11 | ||
|
|
2ff3d9c601 | ||
|
|
d342fbf29f | ||
|
|
f865018cd6 | ||
|
|
3b9a4f0834 | ||
|
|
67bc305950 | ||
|
|
97f128d59f | ||
|
|
e25885c4d9 | ||
|
|
c285026339 | ||
|
|
6c34045a48 | ||
|
|
e1e6261b4f | ||
|
|
697bcb5922 | ||
|
|
e05b0a2491 | ||
|
|
e1655455d8 | ||
|
|
9921dab064 | ||
|
|
403ab7f0f0 | ||
|
|
9bc7ca4094 | ||
|
|
e924aa76b0 | ||
|
|
0c46606e14 | ||
|
|
dbfa4049f5 | ||
|
|
622fe2a8f8 | ||
|
|
7bce5c4f6a | ||
|
|
1f7f2c8971 | ||
|
|
437a849c2a | ||
|
|
8f7261dd50 | ||
|
|
76fa82c3a7 | ||
|
|
46c9774efa | ||
|
|
1bc64a23e3 | ||
|
|
cc9a17afa3 | ||
|
|
fcbcfc3a03 | ||
|
|
0144f3b131 | ||
|
|
518436d5ca | ||
|
|
1961a6b913 | ||
|
|
5edebdd5f3 | ||
|
|
48edf2c212 | ||
|
|
9b16dca9da | ||
|
|
9479c2498c | ||
|
|
4a18bcabe2 | ||
|
|
34f690c53d | ||
|
|
e70cd6579f | ||
|
|
99c047e9c7 | ||
|
|
978ae326f8 | ||
|
|
b01f27f2d3 | ||
|
|
4d34e0dd96 | ||
|
|
9b0065e533 | ||
|
|
34a51feee9 | ||
|
|
679091de59 | ||
|
|
c6220cb920 | ||
|
|
633b30269c | ||
|
|
8ad26267d8 | ||
|
|
7cfb125682 | ||
|
|
a2dc5de5ce | ||
|
|
4f41ff29ec | ||
|
|
13d95b8c19 | ||
|
|
335c13ab44 | ||
|
|
015cdc10a7 | ||
|
|
a5075ab28f | ||
|
|
2e3c3274c7 | ||
|
|
443c581955 | ||
|
|
5e41faf590 | ||
|
|
0579fb83d3 | ||
|
|
2864845d5b | ||
|
|
659dddace7 | ||
|
|
b096f30558 | ||
|
|
dcfb72143a | ||
|
|
c63bcd7706 | ||
|
|
bac306f9b3 | ||
|
|
a6fd96e97b | ||
|
|
7edd91f923 | ||
|
|
66df445bd5 | ||
|
|
5c03034bb4 | ||
|
|
0eab181d46 | ||
|
|
f6f95308c7 | ||
|
|
e93cad91b4 | ||
|
|
08ee61b179 | ||
|
|
0ab756665e | ||
|
|
3570cddce0 | ||
|
|
a2d9f1a5db | ||
|
|
0c6a214e55 | ||
|
|
095fcdbe69 | ||
|
|
3afb1d18f6 | ||
|
|
4526c84330 | ||
|
|
a9cc1cb4b9 | ||
|
|
7acf1eace3 | ||
|
|
bb2e8cb287 | ||
|
|
3b389259c1 | ||
|
|
310ce55b09 | ||
|
|
4662e3fec2 | ||
|
|
10fab9d415 | ||
|
|
acecf01cbe | ||
|
|
b95fb9cda4 | ||
|
|
23fd595582 | ||
|
|
468d05cb10 | ||
|
|
88c2e5d160 | ||
|
|
8cf491b802 | ||
|
|
5886e78299 | ||
|
|
591ef9781c | ||
|
|
52fef96c1c | ||
|
|
d5fd9c57ec | ||
|
|
8a7d6a1c13 | ||
|
|
1745c72981 | ||
|
|
b98b7646cb | ||
|
|
1446e925ec | ||
|
|
799cda1e3e | ||
|
|
d2e0231341 | ||
|
|
9e0aa655a0 | ||
|
|
e303c73d44 | ||
|
|
e5e8e66d6b | ||
|
|
0bb3381122 | ||
|
|
2c5bf2bd6b | ||
|
|
83763b00ae | ||
|
|
47fc795c97 | ||
|
|
6a59bf7af3 | ||
|
|
6f185570d6 | ||
|
|
8b8336d9d0 | ||
|
|
a1e1d6ed3f | ||
|
|
df3e209534 | ||
|
|
851408860e | ||
|
|
d92ea23b0e | ||
|
|
d83641db92 | ||
|
|
48f262e1f3 | ||
|
|
bc49a7db50 | ||
|
|
49975b461c | ||
|
|
3ccb635956 | ||
|
|
074a90e0ac | ||
|
|
526f927436 | ||
|
|
fae7fad915 | ||
|
|
56132ceb8b | ||
|
|
3c3fce0a5a | ||
|
|
8139384961 | ||
|
|
734cc94048 | ||
|
|
91dbb7770d | ||
|
|
707709837d | ||
|
|
e868e65208 | ||
|
|
b19ad2a13b | ||
|
|
98ccccf6ab | ||
|
|
0a6166c557 | ||
|
|
5ee00e52a8 | ||
|
|
ff2b4e0b2f | ||
|
|
8fdd8db51b | ||
|
|
f113eb7ba8 | ||
|
|
40bc25ed5f | ||
|
|
cd3eab5d35 | ||
|
|
7038e15ad2 | ||
|
|
c72abd29ec | ||
|
|
5efc3d2228 | ||
|
|
8ac3f83c79 | ||
|
|
b31b054d0c | ||
|
|
fb4a87f5ce | ||
|
|
049c73880b | ||
|
|
7b2c54f6a6 | ||
|
|
23bda39812 | ||
|
|
90af5801ee | ||
|
|
543c241bc6 | ||
|
|
19a58a3510 | ||
|
|
73c6fd18ed | ||
|
|
409297c6aa | ||
|
|
4272b9ea35 | ||
|
|
3c5c5b0bbc | ||
|
|
802b2d722e | ||
|
|
8f90d3f8ff | ||
|
|
f93cbbf508 | ||
|
|
2ba8756bd3 | ||
|
|
8013de56c9 | ||
|
|
4af9b22442 | ||
|
|
8a392170cf | ||
|
|
6385e053ae | ||
|
|
2789119882 | ||
|
|
3e0553c3e8 | ||
|
|
5e16ace1e7 | ||
|
|
0ca23cc4ca | ||
|
|
4949457001 | ||
|
|
8ae13fe823 | ||
|
|
fdf672092a | ||
|
|
48dd6ea826 | ||
|
|
68bb4f15e2 | ||
|
|
9c6d9ec22d | ||
|
|
ac075ace73 | ||
|
|
eb58774ee9 | ||
|
|
04613bf9a9 | ||
|
|
4194f1964d | ||
|
|
42c64c82d7 | ||
|
|
d44a8966f2 | ||
|
|
ff44330aa1 | ||
|
|
ed259e8168 | ||
|
|
444836975d | ||
|
|
3756fa93e4 | ||
|
|
064b52b306 | ||
|
|
648dc51474 | ||
|
|
7c0bc32759 | ||
|
|
d4b6cc5e4d | ||
|
|
75fca715e3 | ||
|
|
6e9a4a29ef | ||
|
|
78f643c50f | ||
|
|
2db4f1ba87 | ||
|
|
5741dbb78c |
+3
-1
@@ -1,3 +1,4 @@
|
|||||||
|
UPDATE_PORT=5300
|
||||||
PUBLIC_PORT=5400
|
PUBLIC_PORT=5400
|
||||||
CRON_PORT=5500
|
CRON_PORT=5500
|
||||||
BACK_PORT=5600
|
BACK_PORT=5600
|
||||||
@@ -9,4 +10,5 @@ SECRET='whyour'
|
|||||||
|
|
||||||
QINIU_AK=''
|
QINIU_AK=''
|
||||||
QINIU_SK=''
|
QINIU_SK=''
|
||||||
QINIU_SCOPE=''
|
QINIU_SCOPE=''
|
||||||
|
TEMP=''
|
||||||
|
|||||||
@@ -2,31 +2,55 @@ name: Build And Push Docker Image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- "*.md"
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- "master"
|
||||||
- 'develop'
|
- "develop"
|
||||||
# Sequence of patterns matched against refs/tags
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
- "v*"
|
||||||
schedule:
|
schedule:
|
||||||
# 参考 https://jasonet.co/posts/scheduled-actions/
|
- cron: "00 20 * * *"
|
||||||
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
|
|
||||||
# minute hour day month dayOfWeek
|
|
||||||
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
|
|
||||||
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
to_gitlab:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: pixta-dev/repository-mirroring-action@v1
|
||||||
|
with:
|
||||||
|
target_repo_url: git@gitlab.com:whyour/qinglong.git
|
||||||
|
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||||
|
|
||||||
|
to_gitee:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: pixta-dev/repository-mirroring-action@v1
|
||||||
|
with:
|
||||||
|
target_repo_url: git@gitee.com:whyour/qinglong.git
|
||||||
|
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||||
|
|
||||||
build-static:
|
build-static:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: "8.3.1"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: build front and back
|
- name: build front and back
|
||||||
run: |
|
run: |
|
||||||
npm i -g pnpm
|
pnpm install --frozen-lockfile
|
||||||
pnpm install
|
|
||||||
pnpm build:front
|
pnpm build:front
|
||||||
pnpm build:back
|
pnpm build:back
|
||||||
|
|
||||||
@@ -34,6 +58,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
||||||
GITHUB_BRANCH: ${{ github.ref_name }}
|
GITHUB_BRANCH: ${{ github.ref_name }}
|
||||||
|
REPO_GITEE: git@gitee.com:whyour/qinglong-static.git
|
||||||
|
REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git
|
||||||
|
PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p tmp
|
mkdir -p tmp
|
||||||
cd ./tmp
|
cd ./tmp
|
||||||
@@ -44,34 +71,50 @@ jobs:
|
|||||||
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
||||||
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
|
||||||
|
git remote add gitee "${REPO_GITEE}"
|
||||||
|
git remote add gitlab "${REPO_GITLAB}"
|
||||||
|
git gc
|
||||||
|
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||||
|
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: build-static
|
needs: build-static
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
# runs-on: self-hosted
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: pnpm/action-setup@v3
|
||||||
|
|
||||||
- name: Set time zone
|
|
||||||
uses: szenius/set-timezone@v1.0
|
|
||||||
with:
|
with:
|
||||||
timezoneLinux: "Asia/Shanghai"
|
version: "8.3.1"
|
||||||
timezoneMacos: "Asia/Shanghai"
|
- uses: actions/setup-node@v4
|
||||||
timezoneWindows: "China Standard Time"
|
with:
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Setup timezone
|
||||||
|
uses: szenius/set-timezone@v2.0
|
||||||
|
with:
|
||||||
|
timezoneLinux: Asia/Shanghai
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -79,38 +122,114 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ github.repository }}
|
${{ github.repository }}
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
# generate Docker tags based on the following events/attributes
|
# generate Docker tags based on the following events/attributes
|
||||||
# nightly, master, pr-2, 1.2.3, 1.2, 1
|
# nightly, master, pr-2, 1.2.3, 1.2, 1
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
tags: |
|
tags: |
|
||||||
type=schedule,pattern=nightly
|
type=schedule,pattern=nightly
|
||||||
type=edge
|
type=edge
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
|
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
build-args: |
|
build-args: |
|
||||||
MAINTAINER=${{ github.repository_owner }}
|
MAINTAINER=${{ github.repository_owner }}
|
||||||
QL_BRANCH=${{ github.ref_name }}
|
QL_BRANCH=${{ github.ref_name }}
|
||||||
|
SOURCE_COMMIT=${{ github.sha }}
|
||||||
network: host
|
network: host
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||||
context: docker/
|
# platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||||
|
context: .
|
||||||
|
file: ./docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=whyour/qinglong:cache
|
||||||
|
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: |
|
||||||
|
echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
||||||
|
build310:
|
||||||
|
if: ${{ github.ref_name == 'master' }}
|
||||||
|
needs: build-static
|
||||||
|
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: "8.3.1"
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Setup timezone
|
||||||
|
uses: szenius/set-timezone@v2.0
|
||||||
|
with:
|
||||||
|
timezoneLinux: Asia/Shanghai
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push python3.10
|
||||||
|
id: docker_build_310
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
build-args: |
|
||||||
|
MAINTAINER=${{ github.repository_owner }}
|
||||||
|
QL_BRANCH=${{ github.ref_name }}
|
||||||
|
SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
network: host
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||||
|
context: .
|
||||||
|
file: ./docker/310.Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: whyour/qinglong:python3.10
|
||||||
|
cache-from: type=registry,ref=whyour/qinglong:cache-python3.10
|
||||||
|
cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: |
|
||||||
|
echo ${{ steps.docker_build_310.outputs.digest }}
|
||||||
|
|||||||
+5
-8
@@ -5,7 +5,6 @@
|
|||||||
/npm-debug.log*
|
/npm-debug.log*
|
||||||
/yarn-error.log
|
/yarn-error.log
|
||||||
/yarn.lock
|
/yarn.lock
|
||||||
/pnpm-lock.yaml
|
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
|
||||||
# production
|
# production
|
||||||
@@ -23,10 +22,8 @@
|
|||||||
.env
|
.env
|
||||||
.history
|
.history
|
||||||
.version.ts
|
.version.ts
|
||||||
|
/.tmp
|
||||||
/config
|
__pycache__
|
||||||
/log
|
/shell/preload/env.*
|
||||||
/db
|
/shell/preload/notify.*
|
||||||
/manual_log
|
/shell/preload/*-notify.json
|
||||||
/scripts
|
|
||||||
/bak
|
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
|
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
+20
-4
@@ -2,7 +2,23 @@
|
|||||||
**/*.svg
|
**/*.svg
|
||||||
**/*.ejs
|
**/*.ejs
|
||||||
**/*.html
|
**/*.html
|
||||||
package.json
|
/.umi
|
||||||
.umi
|
/.umi-production
|
||||||
.umi-production
|
/.umi-test
|
||||||
.umi-test
|
/.history
|
||||||
|
/.tmp
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-error.log
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
/static
|
||||||
|
/data
|
||||||
|
DS_Store
|
||||||
|
/src/.umi
|
||||||
|
/src/.umi-production
|
||||||
|
/src/.umi-test
|
||||||
|
.env.local
|
||||||
|
.env
|
||||||
|
version.ts
|
||||||
|
/.tmp
|
||||||
@@ -1,24 +1,36 @@
|
|||||||
import { defineConfig } from '@umijs/max';
|
import { defineConfig } from '@umijs/max';
|
||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
|
|
||||||
|
const baseUrl = process.env.QlBaseUrl || '/';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
hash: true,
|
hash: true,
|
||||||
|
jsMinifier: 'terser',
|
||||||
antd: {},
|
antd: {},
|
||||||
|
locale: {
|
||||||
|
antd: true,
|
||||||
|
title: true,
|
||||||
|
baseNavigator: true,
|
||||||
|
},
|
||||||
outputPath: 'static/dist',
|
outputPath: 'static/dist',
|
||||||
fastRefresh: true,
|
fastRefresh: true,
|
||||||
favicons: ['/images/favicon.svg'],
|
favicons: [`https://qn.whyour.cn/favicon.svg`],
|
||||||
mfsu: {
|
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||||
strategy: 'eager',
|
|
||||||
},
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/public': {
|
[`${baseUrl}api/update`]: {
|
||||||
|
target: 'http://127.0.0.1:5300/',
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: { [`^${baseUrl}api/update`]: '/api' },
|
||||||
|
},
|
||||||
|
[`${baseUrl}api/public`]: {
|
||||||
target: 'http://127.0.0.1:5400/',
|
target: 'http://127.0.0.1:5400/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
|
||||||
},
|
},
|
||||||
'/api': {
|
[`${baseUrl}api`]: {
|
||||||
target: 'http://127.0.0.1:5600/',
|
target: 'http://127.0.0.1:5600/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
|
pathRewrite: { [`^${baseUrl}api`]: '/api' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
chainWebpack: ((config: any) => {
|
chainWebpack: ((config: any) => {
|
||||||
@@ -31,12 +43,12 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}) as any,
|
}) as any,
|
||||||
externals: {
|
headScripts: [`./api/env.js`],
|
||||||
react: 'window.React',
|
copy: [
|
||||||
'react-dom': 'window.ReactDOM',
|
{
|
||||||
},
|
from: 'node_modules/monaco-editor/min/vs',
|
||||||
headScripts: [
|
to: 'static/dist/monaco-editor/min/vs',
|
||||||
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
|
},
|
||||||
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
|
|
||||||
],
|
],
|
||||||
|
npmClient: 'pnpm',
|
||||||
});
|
});
|
||||||
|
|||||||
+53
-168
@@ -1,30 +1,29 @@
|
|||||||
<p align="center">
|
|
||||||
<a href="https://github.com/whyour/qinglong">
|
|
||||||
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">Green Dragon</h1>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||||
|
|
||||||
Timed task management panel with python3, javaScript, shell, typescript support
|
<h1 align="center">Qinglong</h1>
|
||||||
|
|
||||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
[简体中文](./README.md) | English
|
||||||
|
|
||||||
|
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||||
|
|
||||||
|
[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||||
|
|
||||||
|
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
|
||||||
|
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
|
||||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
|
||||||
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
|
|
||||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
||||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
|
|
||||||
|
[Demo](http://demo.ninesix.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
||||||
|
|
||||||
|
[演示](http://demo.ninesix.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[](https://whyour.cn)
|

|
||||||
|
|
||||||
[简体中文](./README.md) | English
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -36,146 +35,50 @@ Timed task management panel with python3, javaScript, shell, typescript support
|
|||||||
- Support dark mode
|
- Support dark mode
|
||||||
- Support cell phone operation
|
- Support cell phone operation
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
### docker
|
||||||
|
|
||||||
|
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull whyour/qinglong:latest
|
||||||
|
docker pull whyour/qinglong:debian
|
||||||
|
```
|
||||||
|
|
||||||
|
### npm
|
||||||
|
|
||||||
|
The npm version supports `debian/ubuntu/alpine` systems and requires `node/npm/python3/pip3/pnpm` to be installed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @whyour/qinglong
|
||||||
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Local Deployment
|
[View Documentation](https://qinglong.online/guide/getting-started/installation-guide)
|
||||||
|
|
||||||
|
## Built-in API
|
||||||
|
|
||||||
|
[View Documentation](https://qinglong.online/guide/user-guide/built-in-api)
|
||||||
|
|
||||||
|
## Built-in commands
|
||||||
|
|
||||||
|
[View Documentation](https://qinglong.online/guide/user-guide/basic-explanation)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# To be completed
|
git clone https://github.com/whyour/qinglong.git
|
||||||
|
cd qinglong
|
||||||
|
cp .env.example .env
|
||||||
|
# Recommended use pnpm https://pnpm.io/zh/installation
|
||||||
|
npm install -g pnpm@8.3.1
|
||||||
|
pnpm install
|
||||||
|
pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Podman Deployment
|
Open your browser and visit <http://127.0.0.1:5700>
|
||||||
|
|
||||||
1. podman installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
https://podman.io/getting-started/installation
|
|
||||||
```
|
|
||||||
|
|
||||||
2. start the container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
podman run -dit \
|
|
||||||
--network bridge \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
-p 5700:5700 \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
--restart unless-stopped \
|
|
||||||
docker.io/whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Deployment
|
|
||||||
|
|
||||||
1. docker installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -sSL get.docker.com | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. configure domestic mirror sources
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /etc/docker
|
|
||||||
tee /etc/docker/daemon.json <<-'EOF'
|
|
||||||
{
|
|
||||||
"registry-mirrors": [
|
|
||||||
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
|
||||||
"https://ypzju6vq.mirror.aliyuncs.com",
|
|
||||||
"https://registry.docker-cn.com",
|
|
||||||
"http://hub-mirror.c.163.com",
|
|
||||||
"https://docker.mirrors.ustc.edu.cn"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart docker
|
|
||||||
```
|
|
||||||
|
|
||||||
3. start the container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -dit \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
-p 5700:5700 \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
--restart unless-stopped \
|
|
||||||
whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker-compose Deployment
|
|
||||||
|
|
||||||
1. docker-compose installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
2. start the container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir qinglong
|
|
||||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
|
||||||
|
|
||||||
# start
|
|
||||||
docker-compose up -d
|
|
||||||
# stop
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
3. access
|
|
||||||
|
|
||||||
Open your browser and visit http://{ip}:5700
|
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
1. built-in commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Update and restart Green Dragon
|
|
||||||
ql update
|
|
||||||
# Run custom scripts extra.sh
|
|
||||||
ql extra
|
|
||||||
# Adding a single script file
|
|
||||||
ql raw <file_url>
|
|
||||||
# Add a specific script for a single repository
|
|
||||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
|
||||||
# Delete old logs
|
|
||||||
ql rmlog <days>
|
|
||||||
# Start bot
|
|
||||||
ql bot
|
|
||||||
# Detecting the Green Dragon environment and repairing it
|
|
||||||
ql check
|
|
||||||
# Reset the number of login errors
|
|
||||||
ql resetlet
|
|
||||||
# Disable two-step login
|
|
||||||
ql resettfa
|
|
||||||
|
|
||||||
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
|
|
||||||
task <file_path>
|
|
||||||
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
|
|
||||||
# and the foreground will output the day, while recorded in the log file
|
|
||||||
task <file_path> now
|
|
||||||
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
|
|
||||||
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
|
|
||||||
task <file_path> conc <env_name> <account_number>(Optional)
|
|
||||||
# Specify the account to execute and run immediately regardless of whether a random delay is set
|
|
||||||
task <file_path> desi <env_name> <account_number>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. parameter description
|
|
||||||
|
|
||||||
* file_url: Script address
|
|
||||||
* repo_url: Repository address
|
|
||||||
* whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
|
|
||||||
* blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
|
|
||||||
* dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
|
|
||||||
* branch: Pull the branch of the repository
|
|
||||||
* days: Number of days of logs to be kept
|
|
||||||
* file_path: File path for task execution
|
|
||||||
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
|
|
||||||
* account_number: Specify the account number of an environment variable to be executed when the task is executed
|
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
@@ -187,28 +90,10 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
- [darkreader](https://github.com/darkreader/darkreader)
|
- [darkreader](https://github.com/darkreader/darkreader)
|
||||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ git clone git@github.com:whyour/qinglong.git
|
|
||||||
$ cd qinglong
|
|
||||||
$ cp .env.example .env
|
|
||||||
# Recommended use pnpm https://pnpm.io/zh/installation
|
|
||||||
$ npm install -g pnpm
|
|
||||||
$ pnpm install
|
|
||||||
$ pnpm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Open your browser and visit http://127.0.0.1:5700
|
|
||||||
|
|
||||||
## Communication
|
|
||||||
|
|
||||||
[telegram channel](https://t.me/jiao_long)
|
|
||||||
|
|
||||||
## Name Origin
|
## Name Origin
|
||||||
|
|
||||||
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
|
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
|
||||||
|
|
||||||
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
|
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
|
||||||
|
|
||||||
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
<p align="center">
|
<div align="center">
|
||||||
<a href="https://github.com/whyour/qinglong">
|
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||||
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">青龙</h1>
|
<h1 align="center">青龙</h1>
|
||||||
|
|
||||||
<div align="center">
|
简体中文 | [English](./README-en.md)
|
||||||
|
|
||||||
支持python3、javaScript、shell、typescript 的定时任务管理面板
|
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台
|
||||||
|
|
||||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||||
|
|
||||||
|
[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||||
|
|
||||||
|
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
|
||||||
|
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
|
||||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
|
||||||
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
|
|
||||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
||||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
|
|
||||||
|
[Demo](http://demo.ninesix.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
||||||
|
|
||||||
|
[演示](http://demo.ninesix.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[](https://whyour.cn)
|

|
||||||
|
|
||||||
简体中文 | [English](./README-en.md)
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
@@ -36,146 +37,50 @@
|
|||||||
- 支持暗黑模式
|
- 支持暗黑模式
|
||||||
- 支持手机端操作
|
- 支持手机端操作
|
||||||
|
|
||||||
|
## 版本
|
||||||
|
|
||||||
|
### docker
|
||||||
|
|
||||||
|
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull whyour/qinglong:latest
|
||||||
|
docker pull whyour/qinglong:debian
|
||||||
|
```
|
||||||
|
|
||||||
|
### npm
|
||||||
|
|
||||||
|
npm 版本支持 `debian/ubuntu/alpine` 系统,需要自行安装 `node/npm/python3/pip3/pnpm`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @whyour/qinglong
|
||||||
|
```
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
### 本机部署
|
[查看文档](https://qinglong.online/guide/getting-started/installation-guide)
|
||||||
|
|
||||||
|
## 内置 API
|
||||||
|
|
||||||
|
[查看文档](https://qinglong.online/guide/user-guide/built-in-api)
|
||||||
|
|
||||||
|
## 内置命令
|
||||||
|
|
||||||
|
[查看文档](https://qinglong.online/guide/user-guide/basic-explanation)
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 待完善
|
git clone https://github.com/whyour/qinglong.git
|
||||||
|
cd qinglong
|
||||||
|
cp .env.example .env
|
||||||
|
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
||||||
|
npm install -g pnpm@8.3.1
|
||||||
|
pnpm install
|
||||||
|
pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
### podman 部署
|
打开你的浏览器,访问 <http://127.0.0.1:5700>
|
||||||
|
|
||||||
1. podman 安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
https://podman.io/getting-started/installation
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 启动容器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
podman run -dit \
|
|
||||||
--network bridge \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
-p 5700:5700 \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
--restart unless-stopped \
|
|
||||||
docker.io/whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### docker 部署
|
|
||||||
|
|
||||||
1. docker 安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -sSL get.docker.com | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 配置国内镜像源
|
|
||||||
Configure domestic mirror sources
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /etc/docker
|
|
||||||
tee /etc/docker/daemon.json <<-'EOF'
|
|
||||||
{
|
|
||||||
"registry-mirrors": [
|
|
||||||
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
|
||||||
"https://ypzju6vq.mirror.aliyuncs.com",
|
|
||||||
"https://registry.docker-cn.com",
|
|
||||||
"http://hub-mirror.c.163.com",
|
|
||||||
"https://docker.mirrors.ustc.edu.cn"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart docker
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 启动容器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -dit \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
-p 5700:5700 \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
--restart unless-stopped \
|
|
||||||
whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### docker-compose 部署
|
|
||||||
|
|
||||||
1. docker-compose 安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 启动容器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir qinglong
|
|
||||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
|
||||||
|
|
||||||
# 启动
|
|
||||||
docker-compose up -d
|
|
||||||
# 停止
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 访问
|
|
||||||
|
|
||||||
打开你的浏览器,访问 http://{ip}:5700
|
|
||||||
|
|
||||||
## 使用
|
|
||||||
|
|
||||||
1. 内置命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 更新并重启青龙
|
|
||||||
ql update
|
|
||||||
# 运行自定义脚本extra.sh
|
|
||||||
ql extra
|
|
||||||
# 添加单个脚本文件
|
|
||||||
ql raw <file_url>
|
|
||||||
# 添加单个仓库的指定脚本
|
|
||||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
|
||||||
# 删除旧日志
|
|
||||||
ql rmlog <days>
|
|
||||||
# 启动tg-bot
|
|
||||||
ql bot
|
|
||||||
# 检测青龙环境并修复
|
|
||||||
ql check
|
|
||||||
# 重置登录错误次数
|
|
||||||
ql resetlet
|
|
||||||
# 禁用两步登录
|
|
||||||
ql resettfa
|
|
||||||
|
|
||||||
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
|
||||||
task <file_path>
|
|
||||||
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
|
||||||
task <file_path> now
|
|
||||||
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
|
||||||
task <file_path> conc <env_name> <account_number>(可选的)
|
|
||||||
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
|
||||||
task <file_path> desi <env_name> <account_number>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 参数说明
|
|
||||||
|
|
||||||
* file_url: 脚本地址
|
|
||||||
* repo_url: 仓库地址
|
|
||||||
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
|
|
||||||
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
|
|
||||||
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
|
|
||||||
* extensions: 拉取仓库的文件后缀,多个竖线分割
|
|
||||||
* branch: 拉取仓库的分支
|
|
||||||
* days: 需要保留的日志的天数
|
|
||||||
* file_path: 任务执行时的文件路径
|
|
||||||
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
|
||||||
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
|
||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
@@ -187,24 +92,6 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
- [darkreader](https://github.com/darkreader/darkreader)
|
- [darkreader](https://github.com/darkreader/darkreader)
|
||||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||||
|
|
||||||
## 开发
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ git clone git@github.com:whyour/qinglong.git
|
|
||||||
$ cd qinglong
|
|
||||||
$ cp .env.example .env
|
|
||||||
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
|
||||||
$ npm install -g pnpm
|
|
||||||
$ pnpm install
|
|
||||||
$ pnpm start
|
|
||||||
```
|
|
||||||
|
|
||||||
打开你的浏览器,访问 http://127.0.0.1:5700
|
|
||||||
|
|
||||||
## 交流
|
|
||||||
|
|
||||||
[telegram频道](https://t.me/jiao_long)
|
|
||||||
|
|
||||||
## 名称来源
|
## 名称来源
|
||||||
|
|
||||||
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
To report a vulnerability, please open a private vulnerability report at <https://github.com/whyour/qinglong/security>.
|
||||||
|
|
||||||
|
While the discovery of new vulnerabilities is rare, we also recommend always using the latest versions of Qinglong to ensure your application remains as secure as possible.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'express';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
platform: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
-21
@@ -1,21 +1,38 @@
|
|||||||
import { getFileContentByName, getLastModifyFilePath } from '../config/util';
|
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs/promises';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { SAMPLE_FILES } from '../config/const';
|
||||||
|
import ConfigService from '../services/config';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/configs', route);
|
app.use('/configs', route);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/sample',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
res.send({
|
||||||
|
code: 200,
|
||||||
|
data: SAMPLE_FILES,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/files',
|
'/files',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const fileList = fs.readdirSync(config.configPath, 'utf-8');
|
const fileList = await fs.readdir(config.configPath, 'utf-8');
|
||||||
res.send({
|
res.send({
|
||||||
code: 200,
|
code: 200,
|
||||||
data: fileList
|
data: fileList
|
||||||
@@ -31,24 +48,11 @@ export default (app: Router) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/detail',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
let content = '';
|
const configService = Container.get(ConfigService);
|
||||||
if (config.blackFileList.includes(req.params.file)) {
|
await configService.getFile(req.query.path as string, res);
|
||||||
res.send({ code: 403, message: '文件无法访问' });
|
|
||||||
}
|
|
||||||
if (req.params.file.includes('sample')) {
|
|
||||||
content = getFileContentByName(
|
|
||||||
`${config.samplePath}${req.params.file}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
content = getFileContentByName(
|
|
||||||
`${config.configPath}${req.params.file}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res.send({ code: 200, data: content });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
@@ -70,12 +74,27 @@ export default (app: Router) => {
|
|||||||
if (config.blackFileList.includes(name)) {
|
if (config.blackFileList.includes(name)) {
|
||||||
res.send({ code: 403, message: '文件无法访问' });
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
}
|
}
|
||||||
const path = `${config.configPath}${name}`;
|
let path = join(config.configPath, name);
|
||||||
fs.writeFileSync(path, content);
|
if (name.startsWith('data/scripts/')) {
|
||||||
|
path = join(config.rootPath, name);
|
||||||
|
}
|
||||||
|
await writeFileWithLock(path, content);
|
||||||
res.send({ code: 200, message: '保存成功' });
|
res.send({ code: 200, message: '保存成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/:file',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const configService = Container.get(ConfigService);
|
||||||
|
await configService.getFile(req.params.file, res);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+33
-5
@@ -30,6 +30,7 @@ export default (app: Router) => {
|
|||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
sorts: Joi.array().optional().allow(null),
|
sorts: Joi.array().optional().allow(null),
|
||||||
filters: Joi.array().optional(),
|
filters: Joi.array().optional(),
|
||||||
|
filterRelation: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -51,13 +52,18 @@ export default (app: Router) => {
|
|||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
sorts: Joi.array().optional().allow(null),
|
sorts: Joi.array().optional().allow(null),
|
||||||
filters: Joi.array().optional(),
|
filters: Joi.array().optional(),
|
||||||
|
filterRelation: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const cronViewService = Container.get(CronViewService);
|
const cronViewService = Container.get(CronViewService);
|
||||||
const data = await cronViewService.update(req.body);
|
if (req.body.type === 1) {
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 400, message: '参数错误' });
|
||||||
|
} else {
|
||||||
|
const data = await cronViewService.update(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
@@ -146,6 +152,21 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/detail',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.find(req.query as any);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
@@ -154,6 +175,10 @@ export default (app: Router) => {
|
|||||||
schedule: Joi.string().required(),
|
schedule: Joi.string().required(),
|
||||||
name: Joi.string().optional(),
|
name: Joi.string().optional(),
|
||||||
labels: Joi.array().optional(),
|
labels: Joi.array().optional(),
|
||||||
|
sub_id: Joi.number().optional().allow(null),
|
||||||
|
extra_schedules: Joi.array().optional().allow(null),
|
||||||
|
task_before: Joi.string().optional().allow('').allow(null),
|
||||||
|
task_after: Joi.string().optional().allow('').allow(null),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -310,6 +335,10 @@ export default (app: Router) => {
|
|||||||
command: Joi.string().required(),
|
command: Joi.string().required(),
|
||||||
schedule: Joi.string().required(),
|
schedule: Joi.string().required(),
|
||||||
name: Joi.string().optional().allow(null),
|
name: Joi.string().optional().allow(null),
|
||||||
|
sub_id: Joi.number().optional().allow(null),
|
||||||
|
extra_schedules: Joi.array().optional().allow(null),
|
||||||
|
task_before: Joi.string().optional().allow('').allow(null),
|
||||||
|
task_after: Joi.string().optional().allow('').allow(null),
|
||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -429,13 +458,12 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.status({
|
const data = await cronService.status({
|
||||||
...req.body,
|
...req.body,
|
||||||
status: parseInt(req.body.status),
|
status: req.body.status ? parseInt(req.body.status) : undefined,
|
||||||
pid: parseInt(req.body.pid) || '',
|
pid: req.body.pid ? parseInt(req.body.pid) : undefined,
|
||||||
});
|
});
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -134,4 +134,20 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/cancel',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
await dependenceService.cancel(req.body);
|
||||||
|
return res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+53
-2
@@ -3,8 +3,22 @@ import { Container } from 'typedi';
|
|||||||
import EnvService from '../services/env';
|
import EnvService from '../services/env';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import multer from 'multer';
|
||||||
|
import config from '../config';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { safeJSONParse } from '../config/util';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.scriptPath);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, file.originalname);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/envs', route);
|
app.use('/envs', route);
|
||||||
|
|
||||||
@@ -26,7 +40,9 @@ export default (app: Router) => {
|
|||||||
body: Joi.array().items(
|
body: Joi.array().items(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
value: Joi.string().required(),
|
value: Joi.string().required(),
|
||||||
name: Joi.string().required(),
|
name: Joi.string()
|
||||||
|
.required()
|
||||||
|
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
|
||||||
remarks: Joi.string().optional().allow(''),
|
remarks: Joi.string().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -35,6 +51,9 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
|
if (!req.body?.length) {
|
||||||
|
return res.send({ code: 400, message: '参数不正确' });
|
||||||
|
}
|
||||||
const data = await envService.create(req.body);
|
const data = await envService.create(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -94,7 +113,6 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.move(req.params.id, req.body);
|
const data = await envService.move(req.params.id, req.body);
|
||||||
@@ -177,4 +195,37 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/upload',
|
||||||
|
upload.single('env'),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
|
||||||
|
const parseContent = safeJSONParse(fileContent);
|
||||||
|
const data = Array.isArray(parseContent)
|
||||||
|
? parseContent
|
||||||
|
: [parseContent];
|
||||||
|
if (data.every((x) => x.name && x.value)) {
|
||||||
|
const result = await envService.create(
|
||||||
|
data.map((x) => ({
|
||||||
|
name: x.name,
|
||||||
|
value: x.value,
|
||||||
|
remarks: x.remarks,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return res.send({ code: 200, data: result });
|
||||||
|
} else {
|
||||||
|
return res.send({
|
||||||
|
code: 400,
|
||||||
|
message: '每条数据 name 或者 value 字段不能为空,参考导出文件格式',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+38
-18
@@ -1,10 +1,9 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as fs from 'fs';
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { emptyDir, getFileContentByName, readDirs } from '../config/util';
|
import { getFileContentByName, readDirs, removeAnsi, rmPath } from '../config/util';
|
||||||
import { join } from 'path';
|
import { join, resolve } from 'path';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
const blacklist = ['.tmp'];
|
const blacklist = ['.tmp'];
|
||||||
@@ -15,7 +14,7 @@ export default (app: Router) => {
|
|||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const result = readDirs(config.logPath, config.logPath, blacklist);
|
const result = await readDirs(config.logPath, config.logPath, blacklist);
|
||||||
res.send({
|
res.send({
|
||||||
code: 200,
|
code: 200,
|
||||||
data: result,
|
data: result,
|
||||||
@@ -27,19 +26,45 @@ export default (app: Router) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/detail',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
if (blacklist.includes(req.path)) {
|
const finalPath = resolve(
|
||||||
|
config.logPath,
|
||||||
|
(req.query.path as string) || '',
|
||||||
|
(req.query.file as string) || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
blacklist.includes(req.query.path as string) ||
|
||||||
|
!finalPath.startsWith(config.logPath)
|
||||||
|
) {
|
||||||
return res.send({ code: 403, message: '暂无权限' });
|
return res.send({ code: 403, message: '暂无权限' });
|
||||||
}
|
}
|
||||||
const filePath = join(
|
const content = await getFileContentByName(finalPath);
|
||||||
|
res.send({ code: 200, data: removeAnsi(content) });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/:file',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const finalPath = resolve(
|
||||||
config.logPath,
|
config.logPath,
|
||||||
(req.query.path || '') as string,
|
(req.query.path as string) || '',
|
||||||
req.params.file,
|
(req.params.file as string) || '',
|
||||||
);
|
);
|
||||||
const content = getFileContentByName(filePath);
|
if (
|
||||||
|
blacklist.includes(req.path) ||
|
||||||
|
!finalPath.startsWith(config.logPath)
|
||||||
|
) {
|
||||||
|
return res.send({ code: 403, message: '暂无权限' });
|
||||||
|
}
|
||||||
|
const content = await getFileContentByName(finalPath);
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -53,7 +78,7 @@ export default (app: Router) => {
|
|||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().allow(''),
|
path: Joi.string().allow(''),
|
||||||
type: Joi.string().optional()
|
type: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -64,16 +89,11 @@ export default (app: Router) => {
|
|||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
const filePath = join(config.logPath, path, filename);
|
const filePath = join(config.logPath, path, filename);
|
||||||
if (type === 'directory') {
|
await rmPath(filePath);
|
||||||
emptyDir(filePath);
|
|
||||||
} else {
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
}
|
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
+90
-37
@@ -1,20 +1,14 @@
|
|||||||
import {
|
import { fileExist, readDirs, readDir, rmPath } from '../config/util';
|
||||||
fileExist,
|
|
||||||
getFileContentByName,
|
|
||||||
readDirs,
|
|
||||||
getLastModifyFilePath,
|
|
||||||
readDir,
|
|
||||||
emptyDir,
|
|
||||||
} from '../config/util';
|
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs/promises';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import path, { join, parse } from 'path';
|
import path, { join, parse } from 'path';
|
||||||
import ScriptService from '../services/script';
|
import ScriptService from '../services/script';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
@@ -34,15 +28,33 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let result = [];
|
let result = [];
|
||||||
const blacklist = ['node_modules', '.git'];
|
const blacklist = [
|
||||||
|
'node_modules',
|
||||||
|
'.git',
|
||||||
|
'.pnpm',
|
||||||
|
'pnpm-lock.yaml',
|
||||||
|
'yarn.lock',
|
||||||
|
'package-lock.json',
|
||||||
|
];
|
||||||
if (req.query.path) {
|
if (req.query.path) {
|
||||||
const targetPath = path.join(
|
const targetPath = path.join(
|
||||||
config.scriptPath,
|
config.scriptPath,
|
||||||
req.query.path as string,
|
req.query.path as string,
|
||||||
);
|
);
|
||||||
result = readDir(targetPath, config.scriptPath, blacklist);
|
result = await readDir(targetPath, config.scriptPath, blacklist);
|
||||||
} else {
|
} else {
|
||||||
result = readDirs(config.scriptPath, config.scriptPath, blacklist);
|
result = await readDirs(
|
||||||
|
config.scriptPath,
|
||||||
|
config.scriptPath,
|
||||||
|
blacklist,
|
||||||
|
(a, b) => {
|
||||||
|
if (a.type === b.type) {
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
} else {
|
||||||
|
return a.type === 'directory' ? -1 : 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
res.send({
|
res.send({
|
||||||
code: 200,
|
code: 200,
|
||||||
@@ -54,17 +66,31 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/detail',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const scriptService = Container.get(ScriptService);
|
||||||
|
const content = await scriptService.getFile(
|
||||||
|
req.query.path as string,
|
||||||
|
req.query.file as string,
|
||||||
|
);
|
||||||
|
res.send({ code: 200, data: content });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/:file',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const filePath = join(
|
const scriptService = Container.get(ScriptService);
|
||||||
config.scriptPath,
|
const content = await scriptService.getFile(
|
||||||
req.query.path as string,
|
req.query.path as string,
|
||||||
req.params.file,
|
req.params.file,
|
||||||
);
|
);
|
||||||
const content = getFileContentByName(filePath);
|
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -94,7 +120,7 @@ export default (app: Router) => {
|
|||||||
path += '/';
|
path += '/';
|
||||||
}
|
}
|
||||||
if (!path.startsWith('/')) {
|
if (!path.startsWith('/')) {
|
||||||
path = `${config.scriptPath}${path}`;
|
path = join(config.scriptPath, path);
|
||||||
}
|
}
|
||||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||||
return res.send({
|
return res.send({
|
||||||
@@ -104,12 +130,12 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.file) {
|
if (req.file) {
|
||||||
fs.renameSync(req.file.path, join(path, req.file.filename));
|
await fs.rename(req.file.path, join(path, filename));
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directory) {
|
if (directory) {
|
||||||
fs.mkdirSync(join(path, directory), { recursive: true });
|
await fs.mkdir(join(path, directory), { recursive: true });
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,16 +147,17 @@ export default (app: Router) => {
|
|||||||
`${originFilename.replace(/\//g, '')}`,
|
`${originFilename.replace(/\//g, '')}`,
|
||||||
);
|
);
|
||||||
const filePath = join(path, `${filename.replace(/\//g, '')}`);
|
const filePath = join(path, `${filename.replace(/\//g, '')}`);
|
||||||
if (fs.existsSync(originFilePath)) {
|
const fileExists = await fileExist(filePath);
|
||||||
fs.copyFileSync(
|
if (fileExists) {
|
||||||
|
await fs.copyFile(
|
||||||
originFilePath,
|
originFilePath,
|
||||||
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
|
join(config.bakPath, originFilename.replace(/\//g, '')),
|
||||||
);
|
);
|
||||||
if (filename !== originFilename) {
|
if (filename !== originFilename) {
|
||||||
fs.unlinkSync(originFilePath);
|
await rmPath(originFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fs.writeFileSync(filePath, content);
|
await writeFileWithLock(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -156,7 +183,7 @@ export default (app: Router) => {
|
|||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
const filePath = join(config.scriptPath, path, filename);
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
fs.writeFileSync(filePath, content);
|
await writeFileWithLock(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -170,7 +197,7 @@ export default (app: Router) => {
|
|||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().allow(''),
|
path: Joi.string().allow(''),
|
||||||
type: Joi.string().optional()
|
type: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -182,11 +209,7 @@ export default (app: Router) => {
|
|||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
const filePath = join(config.scriptPath, path, filename);
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
if (type === 'directory') {
|
await rmPath(filePath);
|
||||||
emptyDir(filePath);
|
|
||||||
} else {
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
}
|
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -207,7 +230,7 @@ export default (app: Router) => {
|
|||||||
let { filename } = req.body as {
|
let { filename } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
};
|
};
|
||||||
const filePath = `${config.scriptPath}${filename}`;
|
const filePath = join(config.scriptPath, filename);
|
||||||
// const stats = fs.statSync(filePath);
|
// const stats = fs.statSync(filePath);
|
||||||
// res.set({
|
// res.set({
|
||||||
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
||||||
@@ -239,7 +262,7 @@ export default (app: Router) => {
|
|||||||
let { filename, content, path } = req.body;
|
let { filename, content, path } = req.body;
|
||||||
const { name, ext } = parse(filename);
|
const { name, ext } = parse(filename);
|
||||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||||
fs.writeFileSync(filePath, content || '', { encoding: 'utf8' });
|
await writeFileWithLock(filePath, content || '');
|
||||||
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.runScript(filePath);
|
const result = await scriptService.runScript(filePath);
|
||||||
@@ -255,23 +278,53 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
content: Joi.string().optional().allow(''),
|
|
||||||
path: Joi.string().optional().allow(''),
|
path: Joi.string().optional().allow(''),
|
||||||
|
pid: Joi.number().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
let { filename, content, path } = req.body;
|
let { filename, path, pid } = req.body;
|
||||||
const { name, ext } = parse(filename);
|
const { name, ext } = parse(filename);
|
||||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||||
|
const logPath = join(config.logPath, path, `${name}.swap`);
|
||||||
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.stopScript(filePath);
|
const result = await scriptService.stopScript(filePath, pid);
|
||||||
|
setTimeout(() => {
|
||||||
|
rmPath(logPath);
|
||||||
|
}, 3000);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/rename',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().allow(''),
|
||||||
|
newFilename: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
let { filename, path, type, newFilename } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
newFilename: string;
|
||||||
|
};
|
||||||
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
|
const newPath = join(config.scriptPath, path, newFilename);
|
||||||
|
await fs.rename(filePath, newPath);
|
||||||
|
res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default (app: Router) => {
|
|||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const data = await subscriptionService.list(
|
const data = await subscriptionService.list(
|
||||||
req.query.searchValue as string,
|
req.query.searchValue as string,
|
||||||
|
req.query.ids as string,
|
||||||
);
|
);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -49,6 +50,9 @@ export default (app: Router) => {
|
|||||||
sub_after: Joi.string().optional().allow('').allow(null),
|
sub_after: Joi.string().optional().allow('').allow(null),
|
||||||
schedule_type: Joi.string().required(),
|
schedule_type: Joi.string().required(),
|
||||||
alias: Joi.string().required(),
|
alias: Joi.string().required(),
|
||||||
|
proxy: Joi.string().optional().allow('').allow(null),
|
||||||
|
autoAddCron: Joi.boolean().optional().allow('').allow(null),
|
||||||
|
autoDelCron: Joi.boolean().optional().allow('').allow(null),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -177,6 +181,9 @@ export default (app: Router) => {
|
|||||||
sub_before: Joi.string().optional().allow('').allow(null),
|
sub_before: Joi.string().optional().allow('').allow(null),
|
||||||
sub_after: Joi.string().optional().allow('').allow(null),
|
sub_after: Joi.string().optional().allow('').allow(null),
|
||||||
alias: Joi.string().required(),
|
alias: Joi.string().required(),
|
||||||
|
proxy: Joi.string().optional().allow('').allow(null),
|
||||||
|
autoAddCron: Joi.boolean().optional().allow('').allow(null),
|
||||||
|
autoDelCron: Joi.boolean().optional().allow('').allow(null),
|
||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -204,12 +211,16 @@ export default (app: Router) => {
|
|||||||
'/',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
query: Joi.object({
|
||||||
|
force: Joi.boolean().optional(),
|
||||||
|
t: Joi.number(),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const data = await subscriptionService.remove(req.body);
|
const data = await subscriptionService.remove(req.body, req.query);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
|
|||||||
+287
-18
@@ -1,13 +1,30 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs/promises';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
import { EnvModel } from '../data/env';
|
import {
|
||||||
|
getUniqPath,
|
||||||
|
handleLogPath,
|
||||||
|
parseVersion,
|
||||||
|
promiseExec,
|
||||||
|
} from '../config/util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import multer from 'multer';
|
||||||
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.tmpPath);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, 'data.tgz');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/system', route);
|
app.use('/system', route);
|
||||||
@@ -16,19 +33,15 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const authInfo = await userService.getUserInfo();
|
const authInfo = await userService.getAuthInfo();
|
||||||
const envCount = await EnvModel.count();
|
const { version, changeLog, changeLogLink, publishTime } =
|
||||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
await parseVersion(config.versionFile);
|
||||||
|
|
||||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
|
||||||
const version = currentVersionFile.match(versionRegx)![1];
|
|
||||||
|
|
||||||
let isInitialized = true;
|
let isInitialized = true;
|
||||||
if (
|
if (
|
||||||
Object.keys(authInfo).length === 2 &&
|
Object.keys(authInfo).length === 2 &&
|
||||||
authInfo.username === 'admin' &&
|
authInfo.username === 'admin' &&
|
||||||
authInfo.password === 'admin' &&
|
authInfo.password === 'admin'
|
||||||
envCount === 0
|
|
||||||
) {
|
) {
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
}
|
}
|
||||||
@@ -37,6 +50,10 @@ export default (app: Router) => {
|
|||||||
data: {
|
data: {
|
||||||
isInitialized,
|
isInitialized,
|
||||||
version,
|
version,
|
||||||
|
publishTime: dayjs(publishTime).unix(),
|
||||||
|
branch: process.env.QL_BRANCH || 'master',
|
||||||
|
changeLog,
|
||||||
|
changeLogLink,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -46,12 +63,12 @@ export default (app: Router) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/log/remove',
|
'/config',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
const data = await systemService.getLogRemoveFrequency();
|
const data = await systemService.getSystemConfig();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -60,19 +77,16 @@ export default (app: Router) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/log/remove',
|
'/config/log-remove-frequency',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
frequency: Joi.number().required(),
|
logRemoveFrequency: Joi.number().allow(null),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
const result = await systemService.updateLogRemoveFrequency(
|
const result = await systemService.updateLogRemoveFrequency(req.body);
|
||||||
req.body.frequency,
|
|
||||||
);
|
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -80,6 +94,96 @@ export default (app: Router) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/config/cron-concurrency',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
cronConcurrency: Joi.number().allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.updateCronConcurrency(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/config/dependence-proxy',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
dependenceProxy: Joi.string().allow('').allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.updateDependenceProxy(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/config/node-mirror',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
nodeMirror: Joi.string().allow('').allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
res.setHeader('Content-type', 'application/octet-stream');
|
||||||
|
await systemService.updateNodeMirror(req.body, res);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/config/python-mirror',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
pythonMirror: Joi.string().allow('').allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.updatePythonMirror(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/config/linux-mirror',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
linuxMirror: Joi.string().allow('').allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
res.setHeader('Content-type', 'application/octet-stream');
|
||||||
|
await systemService.updateLinuxMirror(req.body, res);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/update-check',
|
'/update-check',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -108,6 +212,25 @@ export default (app: Router) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/reload',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
type: Joi.string().optional().allow('').allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.reloadSystem(req.body.type);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/notify',
|
'/notify',
|
||||||
celebrate({
|
celebrate({
|
||||||
@@ -127,4 +250,150 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/command-run',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
command: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const command = req.body.command;
|
||||||
|
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
|
||||||
|
let id = await promiseExec(idStr);
|
||||||
|
const uniqPath = await getUniqPath(command, id);
|
||||||
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
|
const logPath = `${uniqPath}/${logTime}.log`;
|
||||||
|
res.setHeader('Content-type', 'application/octet-stream');
|
||||||
|
await systemService.run(
|
||||||
|
{ ...req.body, logPath },
|
||||||
|
{
|
||||||
|
onStart: async (cp, startTime) => {
|
||||||
|
res.setHeader('QL-Task-Pid', `${cp.pid}`);
|
||||||
|
},
|
||||||
|
onEnd: async (cp, endTime, diff) => {
|
||||||
|
res.end();
|
||||||
|
},
|
||||||
|
onError: async (message: string) => {
|
||||||
|
res.write(`\n${message}`);
|
||||||
|
const absolutePath = await handleLogPath(logPath);
|
||||||
|
await fs.appendFile(absolutePath, `\n${message}`);
|
||||||
|
},
|
||||||
|
onLog: async (message: string) => {
|
||||||
|
res.write(`\n${message}`);
|
||||||
|
const absolutePath = await handleLogPath(logPath);
|
||||||
|
await fs.appendFile(absolutePath, `\n${message}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/command-stop',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
command: Joi.string().optional(),
|
||||||
|
pid: Joi.number().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.stop(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/data/export',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
await systemService.exportData(res);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/data/import',
|
||||||
|
upload.single('data'),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.importData();
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/log',
|
||||||
|
celebrate({
|
||||||
|
query: {
|
||||||
|
startTime: Joi.string().allow('').optional(),
|
||||||
|
endTime: Joi.string().allow('').optional(),
|
||||||
|
t: Joi.string().optional(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
await systemService.getSystemLog(
|
||||||
|
res,
|
||||||
|
req.query as {
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/log',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
await systemService.deleteSystemLog();
|
||||||
|
res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/auth/reset',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
retries: Joi.number().optional(),
|
||||||
|
twoFactorActivated: Joi.boolean().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
await userService.resetAuthInfo(req.body);
|
||||||
|
res.send({ code: 200, message: '更新成功' });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-1
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
|
|||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
@@ -26,6 +27,10 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/login',
|
'/login',
|
||||||
|
rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 100,
|
||||||
|
}),
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
username: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
@@ -69,6 +74,9 @@ export default (app: Router) => {
|
|||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
|
if (process.env.DeployEnv === 'demo') {
|
||||||
|
return res.send({ code: 450, message: '未知错误' });
|
||||||
|
}
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
await userService.updateUsernameAndPassword(req.body);
|
await userService.updateUsernameAndPassword(req.body);
|
||||||
res.send({ code: 200, message: '更新成功' });
|
res.send({ code: 200, message: '更新成功' });
|
||||||
@@ -82,7 +90,7 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const authInfo = await userService.getUserInfo();
|
const authInfo = await userService.getAuthInfo();
|
||||||
res.send({
|
res.send({
|
||||||
code: 200,
|
code: 200,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
+4
-4
@@ -2,7 +2,6 @@ import 'reflect-metadata'; // We need this in order to use @Decorators
|
|||||||
import config from './config';
|
import config from './config';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -11,16 +10,17 @@ async function startServer() {
|
|||||||
|
|
||||||
await require('./loaders/initFile').default();
|
await require('./loaders/initFile').default();
|
||||||
|
|
||||||
await require('./loaders/sentry').default({ expressApp: app });
|
|
||||||
|
|
||||||
await require('./loaders/app').default({ expressApp: app });
|
await require('./loaders/app').default({ expressApp: app });
|
||||||
|
|
||||||
const server = app
|
const server = app
|
||||||
.listen(config.port, () => {
|
.listen(config.port, '0.0.0.0', () => {
|
||||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
Logger.debug(`✌️ 后端服务启动成功!`);
|
||||||
|
console.debug(`✌️ 后端服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export const LOG_END_SYMBOL = ' ';
|
||||||
|
|
||||||
|
export const TASK_COMMAND = 'task';
|
||||||
|
export const QL_COMMAND = 'ql';
|
||||||
|
|
||||||
|
export const TASK_PREFIX = `${TASK_COMMAND} `;
|
||||||
|
export const QL_PREFIX = `${QL_COMMAND} `;
|
||||||
|
|
||||||
|
export const SAMPLE_FILES = [
|
||||||
|
{
|
||||||
|
title: 'config.sample.sh',
|
||||||
|
value: 'sample/config.sample.sh',
|
||||||
|
target: 'config.sh',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'notify.js',
|
||||||
|
value: 'sample/notify.js',
|
||||||
|
target: 'data/scripts/sendNotify.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'notify.py',
|
||||||
|
value: 'sample/notify.py',
|
||||||
|
target: 'data/scripts/notify.py',
|
||||||
|
},
|
||||||
|
];
|
||||||
+44
-5
@@ -1,6 +1,6 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createRandomString } from './util';
|
import { createRandomString } from './share';
|
||||||
|
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
@@ -14,21 +14,36 @@ if (!process.env.QL_DIR) {
|
|||||||
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
|
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
|
const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
|
||||||
|
|
||||||
const rootPath = process.env.QL_DIR as string;
|
const rootPath = process.env.QL_DIR as string;
|
||||||
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||||
|
|
||||||
const dataPath = path.join(rootPath, 'data/');
|
let dataPath = path.join(rootPath, 'data/');
|
||||||
|
|
||||||
|
if (process.env.QL_DATA_DIR) {
|
||||||
|
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const shellPath = path.join(rootPath, 'shell/');
|
||||||
|
const preloadPath = path.join(shellPath, 'preload/');
|
||||||
|
const tmpPath = path.join(rootPath, '.tmp/');
|
||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
const configPath = path.join(dataPath, 'config/');
|
const configPath = path.join(dataPath, 'config/');
|
||||||
const scriptPath = path.join(dataPath, 'scripts/');
|
const scriptPath = path.join(dataPath, 'scripts/');
|
||||||
|
const repoPath = path.join(dataPath, 'repo/');
|
||||||
const bakPath = path.join(dataPath, 'bak/');
|
const bakPath = path.join(dataPath, 'bak/');
|
||||||
const logPath = path.join(dataPath, 'log/');
|
const logPath = path.join(dataPath, 'log/');
|
||||||
const dbPath = path.join(dataPath, 'db/');
|
const dbPath = path.join(dataPath, 'db/');
|
||||||
const uploadPath = path.join(dataPath, 'upload/');
|
const uploadPath = path.join(dataPath, 'upload/');
|
||||||
|
const sshdPath = path.join(dataPath, 'ssh.d/');
|
||||||
|
const systemLogPath = path.join(dataPath, 'syslog/');
|
||||||
|
|
||||||
const envFile = path.join(configPath, 'env.sh');
|
const envFile = path.join(preloadPath, 'env.sh');
|
||||||
|
const jsEnvFile = path.join(preloadPath, 'env.js');
|
||||||
|
const pyEnvFile = path.join(preloadPath, 'env.py');
|
||||||
|
const jsNotifyFile = path.join(preloadPath, 'notify.js');
|
||||||
|
const pyNotifyFile = path.join(preloadPath, 'notify.py');
|
||||||
const confFile = path.join(configPath, 'config.sh');
|
const confFile = path.join(configPath, 'config.sh');
|
||||||
const crontabFile = path.join(configPath, 'crontab.list');
|
const crontabFile = path.join(configPath, 'crontab.list');
|
||||||
const authConfigFile = path.join(configPath, 'auth.json');
|
const authConfigFile = path.join(configPath, 'auth.json');
|
||||||
@@ -40,7 +55,10 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
|
|||||||
const authError = '错误的用户名密码,请重试';
|
const authError = '错误的用户名密码,请重试';
|
||||||
const loginFaild = '请先登录!';
|
const loginFaild = '请先登录!';
|
||||||
const configString = 'config sample crontab shareCode diy';
|
const configString = 'config sample crontab shareCode diy';
|
||||||
const versionFile = path.join(rootPath, 'src/version.ts');
|
const versionFile = path.join(rootPath, 'version.yaml');
|
||||||
|
const dataTgzFile = path.join(tmpPath, 'data.tgz');
|
||||||
|
const shareShellFile = path.join(shellPath, 'share.sh');
|
||||||
|
const dependenceProxyFile = path.join(configPath, 'dependence-proxy.sh');
|
||||||
|
|
||||||
if (envFound.error) {
|
if (envFound.error) {
|
||||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||||
@@ -50,6 +68,7 @@ export default {
|
|||||||
port: parseInt(process.env.BACK_PORT as string, 10),
|
port: parseInt(process.env.BACK_PORT as string, 10),
|
||||||
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
||||||
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
|
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
|
||||||
|
updatePort: parseInt(process.env.UPDATE_PORT as string, 10),
|
||||||
secret: process.env.SECRET || createRandomString(16, 32),
|
secret: process.env.SECRET || createRandomString(16, 32),
|
||||||
logs: {
|
logs: {
|
||||||
level: process.env.LOG_LEVEL || 'silly',
|
level: process.env.LOG_LEVEL || 'silly',
|
||||||
@@ -58,6 +77,11 @@ export default {
|
|||||||
prefix: '/api',
|
prefix: '/api',
|
||||||
},
|
},
|
||||||
rootPath,
|
rootPath,
|
||||||
|
tmpPath,
|
||||||
|
dataPath,
|
||||||
|
dataTgzFile,
|
||||||
|
shareShellFile,
|
||||||
|
dependenceProxyFile,
|
||||||
configString,
|
configString,
|
||||||
loginFaild,
|
loginFaild,
|
||||||
authError,
|
authError,
|
||||||
@@ -69,17 +93,25 @@ export default {
|
|||||||
sampleFile,
|
sampleFile,
|
||||||
confFile,
|
confFile,
|
||||||
envFile,
|
envFile,
|
||||||
|
jsEnvFile,
|
||||||
|
pyEnvFile,
|
||||||
|
jsNotifyFile,
|
||||||
|
pyNotifyFile,
|
||||||
dbPath,
|
dbPath,
|
||||||
uploadPath,
|
uploadPath,
|
||||||
configPath,
|
configPath,
|
||||||
scriptPath,
|
scriptPath,
|
||||||
|
repoPath,
|
||||||
samplePath,
|
samplePath,
|
||||||
blackFileList: [
|
blackFileList: [
|
||||||
'auth.json',
|
'auth.json',
|
||||||
'config.sh.sample',
|
'config.sh.sample',
|
||||||
'cookie.sh',
|
'cookie.sh',
|
||||||
'crontab.list',
|
'crontab.list',
|
||||||
|
'dependence-proxy.sh',
|
||||||
'env.sh',
|
'env.sh',
|
||||||
|
'env.js',
|
||||||
|
'env.py',
|
||||||
'token.json',
|
'token.json',
|
||||||
],
|
],
|
||||||
writePathList: [configPath, scriptPath],
|
writePathList: [configPath, scriptPath],
|
||||||
@@ -91,8 +123,15 @@ export default {
|
|||||||
'/api/system',
|
'/api/system',
|
||||||
'/api/user/init',
|
'/api/user/init',
|
||||||
'/api/user/notification/init',
|
'/api/user/notification/init',
|
||||||
|
'/open/user/login',
|
||||||
|
'/open/user/two-factor/login',
|
||||||
|
'/open/system',
|
||||||
|
'/open/user/init',
|
||||||
|
'/open/user/notification/init',
|
||||||
],
|
],
|
||||||
versionFile,
|
versionFile,
|
||||||
lastVersionFile,
|
lastVersionFile,
|
||||||
sqliteFile,
|
sqliteFile,
|
||||||
|
sshdPath,
|
||||||
|
systemLogPath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import pick from 'lodash/pick';
|
||||||
|
|
||||||
|
let pickedEnv: Record<string, string>;
|
||||||
|
|
||||||
|
function getPickedEnv() {
|
||||||
|
if (pickedEnv) return pickedEnv;
|
||||||
|
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv', 'QL_DIR']);
|
||||||
|
if (picked.QlBaseUrl) {
|
||||||
|
if (!picked.QlBaseUrl.startsWith('/')) {
|
||||||
|
picked.QlBaseUrl = `/${picked.QlBaseUrl}`;
|
||||||
|
}
|
||||||
|
if (!picked.QlBaseUrl.endsWith('/')) {
|
||||||
|
picked.QlBaseUrl = `${picked.QlBaseUrl}/`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pickedEnv = picked as Record<string, string>;
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serveEnv(_req: Request, res: Response) {
|
||||||
|
res.type('.js');
|
||||||
|
res.send(
|
||||||
|
Object.entries(getPickedEnv())
|
||||||
|
.map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)
|
||||||
|
.join('\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
export function createRandomString(min: number, max: number): string {
|
||||||
|
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
|
const english = [
|
||||||
|
'a',
|
||||||
|
'b',
|
||||||
|
'c',
|
||||||
|
'd',
|
||||||
|
'e',
|
||||||
|
'f',
|
||||||
|
'g',
|
||||||
|
'h',
|
||||||
|
'i',
|
||||||
|
'j',
|
||||||
|
'k',
|
||||||
|
'l',
|
||||||
|
'm',
|
||||||
|
'n',
|
||||||
|
'o',
|
||||||
|
'p',
|
||||||
|
'q',
|
||||||
|
'r',
|
||||||
|
's',
|
||||||
|
't',
|
||||||
|
'u',
|
||||||
|
'v',
|
||||||
|
'w',
|
||||||
|
'x',
|
||||||
|
'y',
|
||||||
|
'z',
|
||||||
|
];
|
||||||
|
const ENGLISH = [
|
||||||
|
'A',
|
||||||
|
'B',
|
||||||
|
'C',
|
||||||
|
'D',
|
||||||
|
'E',
|
||||||
|
'F',
|
||||||
|
'G',
|
||||||
|
'H',
|
||||||
|
'I',
|
||||||
|
'J',
|
||||||
|
'K',
|
||||||
|
'L',
|
||||||
|
'M',
|
||||||
|
'N',
|
||||||
|
'O',
|
||||||
|
'P',
|
||||||
|
'Q',
|
||||||
|
'R',
|
||||||
|
'S',
|
||||||
|
'T',
|
||||||
|
'U',
|
||||||
|
'V',
|
||||||
|
'W',
|
||||||
|
'X',
|
||||||
|
'Y',
|
||||||
|
'Z',
|
||||||
|
];
|
||||||
|
const special = ['-', '_'];
|
||||||
|
const config = num.concat(english).concat(ENGLISH).concat(special);
|
||||||
|
|
||||||
|
const arr = [];
|
||||||
|
arr.push(getOne(num));
|
||||||
|
arr.push(getOne(english));
|
||||||
|
arr.push(getOne(ENGLISH));
|
||||||
|
arr.push(getOne(special));
|
||||||
|
|
||||||
|
const len = min + Math.floor(Math.random() * (max - min + 1));
|
||||||
|
|
||||||
|
for (let i = 4; i < len; i++) {
|
||||||
|
arr.push(config[Math.floor(Math.random() * config.length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newArr = [];
|
||||||
|
for (let j = 0; j < len; j++) {
|
||||||
|
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOne(arr: any[]) {
|
||||||
|
return arr[Math.floor(Math.random() * arr.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArr.join('');
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Subscription } from '../data/subscription';
|
||||||
|
import isNil from 'lodash/isNil';
|
||||||
|
|
||||||
|
export function formatUrl(doc: Subscription) {
|
||||||
|
let url = doc.url;
|
||||||
|
let host = '';
|
||||||
|
if (doc.type === 'private-repo') {
|
||||||
|
if (doc.pull_type === 'ssh-key') {
|
||||||
|
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
|
||||||
|
url = doc.url!.replace(host, doc.alias);
|
||||||
|
} else {
|
||||||
|
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
|
||||||
|
const { username, password } = doc.pull_option as any;
|
||||||
|
url = doc.url!.replace(host, `${username}:${password}@${host}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { url, host };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCommand(doc: Subscription, url?: string) {
|
||||||
|
let command = `SUB_ID=${doc.id} ql `;
|
||||||
|
let _url = url || formatUrl(doc).url;
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
whitelist,
|
||||||
|
blacklist,
|
||||||
|
dependences,
|
||||||
|
branch,
|
||||||
|
extensions,
|
||||||
|
proxy,
|
||||||
|
autoAddCron,
|
||||||
|
autoDelCron,
|
||||||
|
} = doc;
|
||||||
|
if (type === 'file') {
|
||||||
|
command += `raw "${_url}" "${proxy || ''}" "${
|
||||||
|
isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||||
|
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||||
|
} else {
|
||||||
|
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
||||||
|
dependences || ''
|
||||||
|
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${
|
||||||
|
isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||||
|
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
+312
-206
@@ -1,26 +1,41 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
import iconv from 'iconv-lite';
|
import iconv from 'iconv-lite';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
|
import psTreeFun from 'pstree.remy';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { load } from 'js-yaml';
|
||||||
|
import config from './index';
|
||||||
|
import { TASK_COMMAND } from './const';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
export function getFileContentByName(fileName: string) {
|
export * from './share';
|
||||||
if (fs.existsSync(fileName)) {
|
|
||||||
return fs.readFileSync(fileName, 'utf8');
|
export async function getFileContentByName(fileName: string) {
|
||||||
|
const _exsit = await fileExist(fileName);
|
||||||
|
if (_exsit) {
|
||||||
|
return await fs.readFile(fileName, 'utf8');
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLastModifyFilePath(dir: string) {
|
export function removeAnsi(text: string) {
|
||||||
|
return text.replace(/\x1b\[\d+m/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLastModifyFilePath(dir: string) {
|
||||||
let filePath = '';
|
let filePath = '';
|
||||||
|
|
||||||
if (fs.existsSync(dir)) {
|
const _exsit = await fileExist(dir);
|
||||||
const arr = fs.readdirSync(dir);
|
if (_exsit) {
|
||||||
|
const arr = await fs.readdir(dir);
|
||||||
|
|
||||||
arr.forEach((item) => {
|
arr.forEach(async (item) => {
|
||||||
const fullpath = path.join(dir, item);
|
const fullpath = path.join(dir, item);
|
||||||
const stats = fs.statSync(fullpath);
|
const stats = await fs.lstat(fullpath);
|
||||||
if (stats.isFile()) {
|
if (stats.isFile()) {
|
||||||
if (stats.mtimeMs >= 0) {
|
if (stats.mtimeMs >= 0) {
|
||||||
filePath = fullpath;
|
filePath = fullpath;
|
||||||
@@ -31,91 +46,6 @@ export function getLastModifyFilePath(dir: string) {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRandomString(min: number, max: number): string {
|
|
||||||
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
||||||
const english = [
|
|
||||||
'a',
|
|
||||||
'b',
|
|
||||||
'c',
|
|
||||||
'd',
|
|
||||||
'e',
|
|
||||||
'f',
|
|
||||||
'g',
|
|
||||||
'h',
|
|
||||||
'i',
|
|
||||||
'j',
|
|
||||||
'k',
|
|
||||||
'l',
|
|
||||||
'm',
|
|
||||||
'n',
|
|
||||||
'o',
|
|
||||||
'p',
|
|
||||||
'q',
|
|
||||||
'r',
|
|
||||||
's',
|
|
||||||
't',
|
|
||||||
'u',
|
|
||||||
'v',
|
|
||||||
'w',
|
|
||||||
'x',
|
|
||||||
'y',
|
|
||||||
'z',
|
|
||||||
];
|
|
||||||
const ENGLISH = [
|
|
||||||
'A',
|
|
||||||
'B',
|
|
||||||
'C',
|
|
||||||
'D',
|
|
||||||
'E',
|
|
||||||
'F',
|
|
||||||
'G',
|
|
||||||
'H',
|
|
||||||
'I',
|
|
||||||
'J',
|
|
||||||
'K',
|
|
||||||
'L',
|
|
||||||
'M',
|
|
||||||
'N',
|
|
||||||
'O',
|
|
||||||
'P',
|
|
||||||
'Q',
|
|
||||||
'R',
|
|
||||||
'S',
|
|
||||||
'T',
|
|
||||||
'U',
|
|
||||||
'V',
|
|
||||||
'W',
|
|
||||||
'X',
|
|
||||||
'Y',
|
|
||||||
'Z',
|
|
||||||
];
|
|
||||||
const special = ['-', '_'];
|
|
||||||
const config = num.concat(english).concat(ENGLISH).concat(special);
|
|
||||||
|
|
||||||
const arr = [];
|
|
||||||
arr.push(getOne(num));
|
|
||||||
arr.push(getOne(english));
|
|
||||||
arr.push(getOne(ENGLISH));
|
|
||||||
arr.push(getOne(special));
|
|
||||||
|
|
||||||
const len = min + Math.floor(Math.random() * (max - min + 1));
|
|
||||||
|
|
||||||
for (let i = 4; i < len; i++) {
|
|
||||||
arr.push(config[Math.floor(Math.random() * config.length)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newArr = [];
|
|
||||||
for (let j = 0; j < len; j++) {
|
|
||||||
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOne(arr: any[]) {
|
|
||||||
return arr[Math.floor(Math.random() * arr.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return newArr.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getToken(req: any) {
|
export function getToken(req: any) {
|
||||||
const { authorization = '' } = req.headers;
|
const { authorization = '' } = req.headers;
|
||||||
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
||||||
@@ -136,7 +66,8 @@ export async function getNetIp(req: any) {
|
|||||||
...req.ips,
|
...req.ips,
|
||||||
req.socket.remoteAddress,
|
req.socket.remoteAddress,
|
||||||
]),
|
]),
|
||||||
];
|
].filter(Boolean);
|
||||||
|
|
||||||
let ip = ipArray[0];
|
let ip = ipArray[0];
|
||||||
|
|
||||||
if (ipArray.length > 1) {
|
if (ipArray.length > 1) {
|
||||||
@@ -157,34 +88,42 @@ export async function getNetIp(req: any) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
|
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
|
||||||
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
||||||
ip = '';
|
ip = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ip) {
|
||||||
|
return { address: `获取失败`, ip };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baiduApi = got
|
const csdnApi = got
|
||||||
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
.get(`https://searchplugin.csdn.net/api/v1/ip/get?ip=${ip}`, {
|
||||||
|
timeout: 10000,
|
||||||
|
retry: 0,
|
||||||
|
})
|
||||||
.text();
|
.text();
|
||||||
const ipApi = got
|
const pconlineApi = got
|
||||||
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
|
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
retry: 0,
|
retry: 0,
|
||||||
})
|
})
|
||||||
.buffer();
|
.buffer();
|
||||||
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
|
const [csdnBody, pconlineBody] = await await Promise.all<any>([
|
||||||
|
csdnApi,
|
||||||
const ipRegx = /.*IP :(.*)\n/;
|
pconlineApi,
|
||||||
const addrRegx = /.*数据二 :(.*)\n/;
|
]);
|
||||||
if (data && ipRegx.test(data) && addrRegx.test(data)) {
|
const csdnRes = JSON.parse(csdnBody);
|
||||||
const ip = data.match(ipRegx)[1];
|
const pconlineRes = JSON.parse(iconv.decode(pconlineBody, 'GBK'));
|
||||||
const addr = data.match(addrRegx)[1];
|
let address = '';
|
||||||
return { address: addr, ip };
|
if (csdnBody && csdnRes.code == 200) {
|
||||||
} else if (ipApiBody) {
|
address = csdnRes.data.address;
|
||||||
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
|
} else if (pconlineRes && pconlineRes.addr) {
|
||||||
return { address: `${addr}`, ip };
|
address = pconlineRes.addr;
|
||||||
} else {
|
|
||||||
return { address: `获取失败`, ip };
|
|
||||||
}
|
}
|
||||||
|
return { address, ip };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { address: `获取失败`, ip };
|
return { address: `获取失败`, ip };
|
||||||
}
|
}
|
||||||
@@ -211,12 +150,19 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
|||||||
system = 'android'; // android系统
|
system = 'android'; // android系统
|
||||||
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
||||||
system = 'ios'; // ios系统
|
system = 'ios'; // ios系统
|
||||||
|
} else if (testUa(/openharmony/g)) {
|
||||||
|
system = 'openharmony'; // openharmony系统
|
||||||
}
|
}
|
||||||
|
|
||||||
let platform = 'desktop';
|
let platform = 'desktop';
|
||||||
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
||||||
platform = 'desktop';
|
platform = 'desktop';
|
||||||
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
|
} else if (
|
||||||
|
system === 'android' ||
|
||||||
|
system === 'ios' ||
|
||||||
|
system === 'openharmony' ||
|
||||||
|
testUa(/mobile/g)
|
||||||
|
) {
|
||||||
platform = 'mobile';
|
platform = 'mobile';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,22 +170,29 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fileExist(file: any) {
|
export async function fileExist(file: any) {
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
try {
|
await fs.access(file);
|
||||||
fs.accessSync(file);
|
return true;
|
||||||
resolve(true);
|
} catch (error) {
|
||||||
} catch (error) {
|
return false;
|
||||||
resolve(false);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFile(file: string, data: string = '') {
|
export async function createFile(file: string, data: string = '') {
|
||||||
return new Promise((resolve) => {
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
await writeFileWithLock(file, data);
|
||||||
fs.writeFileSync(file, data);
|
}
|
||||||
resolve(true);
|
|
||||||
});
|
export async function handleLogPath(
|
||||||
|
logPath: string,
|
||||||
|
data: string = '',
|
||||||
|
): Promise<string> {
|
||||||
|
const absolutePath = path.resolve(config.logPath, logPath);
|
||||||
|
const logFileExist = await fileExist(absolutePath);
|
||||||
|
if (!logFileExist) {
|
||||||
|
await createFile(absolutePath, data);
|
||||||
|
}
|
||||||
|
return absolutePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function concurrentRun(
|
export async function concurrentRun(
|
||||||
@@ -284,56 +237,82 @@ enum FileType {
|
|||||||
'file',
|
'file',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDirs(
|
interface IFile {
|
||||||
dir: string,
|
title: string;
|
||||||
baseDir: string = '',
|
key: string;
|
||||||
blacklist: string[] = [],
|
type: 'directory' | 'file';
|
||||||
) {
|
parent: string;
|
||||||
const relativePath = path.relative(baseDir, dir);
|
createTime: number;
|
||||||
const files = fs.readdirSync(dir);
|
size?: number;
|
||||||
const result: any = files
|
children?: IFile[];
|
||||||
.filter((x) => !blacklist.includes(x))
|
|
||||||
.map((file: string) => {
|
|
||||||
const subPath = path.join(dir, file);
|
|
||||||
const stats = fs.statSync(subPath);
|
|
||||||
const key = path.join(relativePath, file);
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
return {
|
|
||||||
title: file,
|
|
||||||
key,
|
|
||||||
type: 'directory',
|
|
||||||
parent: relativePath,
|
|
||||||
children: readDirs(subPath, baseDir).sort(
|
|
||||||
(a: any, b: any) =>
|
|
||||||
(FileType as any)[a.type] - (FileType as any)[b.type],
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
title: file,
|
|
||||||
type: 'file',
|
|
||||||
isLeaf: true,
|
|
||||||
key,
|
|
||||||
parent: relativePath,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return result.sort(
|
|
||||||
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDir(
|
export function dirSort(a: IFile, b: IFile): number {
|
||||||
|
if (a.type === 'file' && b.type === 'file') {
|
||||||
|
return b.createTime - a.createTime;
|
||||||
|
} else if (a.type === 'directory' && b.type === 'directory') {
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
} else {
|
||||||
|
return a.type === 'directory' ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readDirs(
|
||||||
|
dir: string,
|
||||||
|
baseDir: string = '',
|
||||||
|
blacklist: string[] = [],
|
||||||
|
sort: (a: IFile, b: IFile) => number = dirSort,
|
||||||
|
): Promise<IFile[]> {
|
||||||
|
const relativePath = path.relative(baseDir, dir);
|
||||||
|
const files = await fs.readdir(dir);
|
||||||
|
const result: IFile[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const subPath = path.join(dir, file);
|
||||||
|
const stats = await fs.lstat(subPath);
|
||||||
|
const key = path.join(relativePath, file);
|
||||||
|
|
||||||
|
if (blacklist.includes(file) || stats.isSymbolicLink()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
const children = await readDirs(subPath, baseDir, blacklist, sort);
|
||||||
|
result.push({
|
||||||
|
title: file,
|
||||||
|
key,
|
||||||
|
type: 'directory',
|
||||||
|
parent: relativePath,
|
||||||
|
createTime: stats.birthtime.getTime(),
|
||||||
|
children: children.sort(sort),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
title: file,
|
||||||
|
type: 'file',
|
||||||
|
key,
|
||||||
|
parent: relativePath,
|
||||||
|
size: stats.size,
|
||||||
|
createTime: stats.birthtime.getTime(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.sort(sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readDir(
|
||||||
dir: string,
|
dir: string,
|
||||||
baseDir: string = '',
|
baseDir: string = '',
|
||||||
blacklist: string[] = [],
|
blacklist: string[] = [],
|
||||||
) {
|
) {
|
||||||
const relativePath = path.relative(baseDir, dir);
|
const relativePath = path.relative(baseDir, dir);
|
||||||
const files = fs.readdirSync(dir);
|
const files = await fs.readdir(dir);
|
||||||
const result: any = files
|
const result: any = files
|
||||||
.filter((x) => !blacklist.includes(x))
|
.filter((x) => !blacklist.includes(x))
|
||||||
.map((file: string) => {
|
.map(async (file: string) => {
|
||||||
const subPath = path.join(dir, file);
|
const subPath = path.join(dir, file);
|
||||||
const stats = fs.statSync(subPath);
|
const stats = await fs.lstat(subPath);
|
||||||
const key = path.join(relativePath, file);
|
const key = path.join(relativePath, file);
|
||||||
return {
|
return {
|
||||||
title: file,
|
title: file,
|
||||||
@@ -345,30 +324,28 @@ export function readDir(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emptyDir(path: string) {
|
export async function promiseExec(command: string): Promise<string> {
|
||||||
const files = fs.readdirSync(path);
|
try {
|
||||||
files.forEach((file) => {
|
const { stderr, stdout } = await promisify(exec)(command, {
|
||||||
const filePath = `${path}/${file}`;
|
maxBuffer: 200 * 1024 * 1024,
|
||||||
const stats = fs.statSync(filePath);
|
encoding: 'utf8',
|
||||||
if (stats.isDirectory()) {
|
});
|
||||||
emptyDir(filePath);
|
return stdout || stderr;
|
||||||
} else {
|
} catch (error) {
|
||||||
fs.unlinkSync(filePath);
|
return JSON.stringify(error);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
fs.rmdirSync(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function promiseExec(command: string): Promise<string> {
|
export async function promiseExecSuccess(command: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
exec(
|
const { stdout } = await promisify(exec)(command, {
|
||||||
command,
|
maxBuffer: 200 * 1024 * 1024,
|
||||||
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
|
encoding: 'utf8',
|
||||||
(err, stdout, stderr) => {
|
});
|
||||||
resolve(stdout || stderr || JSON.stringify(err));
|
return stdout || '';
|
||||||
},
|
} catch (error) {
|
||||||
);
|
return '';
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseHeaders(headers: string) {
|
export function parseHeaders(headers: string) {
|
||||||
@@ -395,32 +372,49 @@ export function parseHeaders(headers: string) {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseString(
|
||||||
|
input: string,
|
||||||
|
valueFormatFn?: (v: string) => string,
|
||||||
|
): Record<string, string> {
|
||||||
|
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
|
||||||
|
const matches: Record<string, string> = {};
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(input)) !== null) {
|
||||||
|
const [, key, value] = match;
|
||||||
|
const _key = key.trim();
|
||||||
|
if (!_key || matches[_key]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _value = value.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
_value = valueFormatFn ? valueFormatFn(_value) : _value;
|
||||||
|
const jsonValue = JSON.parse(_value);
|
||||||
|
matches[_key] = jsonValue;
|
||||||
|
} catch (error) {
|
||||||
|
matches[_key] = _value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseBody(
|
export function parseBody(
|
||||||
body: string,
|
body: string,
|
||||||
contentType:
|
contentType:
|
||||||
| 'application/json'
|
| 'application/json'
|
||||||
| 'multipart/form-data'
|
| 'multipart/form-data'
|
||||||
| 'application/x-www-form-urlencoded',
|
| 'application/x-www-form-urlencoded'
|
||||||
|
| 'text/plain',
|
||||||
|
valueFormatFn?: (v: string) => string,
|
||||||
) {
|
) {
|
||||||
if (!body) return '';
|
if (contentType === 'text/plain' || !body) {
|
||||||
|
return valueFormatFn && body ? valueFormatFn(body) : body;
|
||||||
|
}
|
||||||
|
|
||||||
const parsed: any = {};
|
const parsed = parseString(body, valueFormatFn);
|
||||||
let key;
|
|
||||||
let val;
|
|
||||||
let i;
|
|
||||||
|
|
||||||
body &&
|
|
||||||
body.split('\n').forEach(function parser(line) {
|
|
||||||
i = line.indexOf(':');
|
|
||||||
key = line.substring(0, i).trim().toLowerCase();
|
|
||||||
val = line.substring(i + 1).trim();
|
|
||||||
|
|
||||||
if (!key || parsed[key]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed[key] = val;
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case 'multipart/form-data':
|
case 'multipart/form-data':
|
||||||
@@ -436,3 +430,115 @@ export function parseBody(
|
|||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function psTree(pid: number): Promise<number[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
psTreeFun(pid, (err: any, pids: number[]) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(pids.filter((x) => !isNaN(x)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function killTask(pid: number) {
|
||||||
|
const pids = await psTree(pid);
|
||||||
|
|
||||||
|
if (pids.length) {
|
||||||
|
try {
|
||||||
|
[pid, ...pids].reverse().forEach((x) => {
|
||||||
|
process.kill(x, 15);
|
||||||
|
});
|
||||||
|
} catch (error) {}
|
||||||
|
} else {
|
||||||
|
process.kill(pid, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPid(cmd: string) {
|
||||||
|
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
|
||||||
|
const pid = await promiseExec(taskCommand);
|
||||||
|
return pid ? Number(pid) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IVersion {
|
||||||
|
version: string;
|
||||||
|
changeLogLink: string;
|
||||||
|
changeLog: string;
|
||||||
|
publishTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseVersion(path: string): Promise<IVersion> {
|
||||||
|
return load(await fs.readFile(path, 'utf8')) as IVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseContentVersion(content: string): IVersion {
|
||||||
|
return load(content) as IVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUniqPath(
|
||||||
|
command: string,
|
||||||
|
id: string,
|
||||||
|
): Promise<string> {
|
||||||
|
let suffix = '';
|
||||||
|
if (/^\d+$/.test(id)) {
|
||||||
|
suffix = `_${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = command.split(/ +/);
|
||||||
|
|
||||||
|
const maxTimeCommandIndex = items.findIndex((x) => x === '-m');
|
||||||
|
if (maxTimeCommandIndex !== -1) {
|
||||||
|
items = items.slice(maxTimeCommandIndex + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let str = items[0];
|
||||||
|
if (items[0] === TASK_COMMAND) {
|
||||||
|
str = items[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dotIndex = str.lastIndexOf('.');
|
||||||
|
|
||||||
|
if (dotIndex !== -1) {
|
||||||
|
str = str.slice(0, dotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slashIndex = str.lastIndexOf('/');
|
||||||
|
|
||||||
|
let tempStr = '';
|
||||||
|
if (slashIndex !== -1) {
|
||||||
|
tempStr = str.slice(0, slashIndex);
|
||||||
|
const _slashIndex = tempStr.lastIndexOf('/');
|
||||||
|
if (_slashIndex !== -1) {
|
||||||
|
tempStr = tempStr.slice(_slashIndex + 1);
|
||||||
|
}
|
||||||
|
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${str}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeJSONParse(value?: string) {
|
||||||
|
if (!value) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[JSON.parse失败]', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rmPath(path: string) {
|
||||||
|
try {
|
||||||
|
const _exsit = await fileExist(path);
|
||||||
|
if (_exsit) {
|
||||||
|
await fs.rm(path, { force: true, recursive: true, maxRetries: 5 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[rmPath失败]', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { sequelize } from '.';
|
|
||||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|
||||||
|
|
||||||
export class AuthInfo {
|
|
||||||
ip?: string;
|
|
||||||
type: AuthDataType;
|
|
||||||
info?: any;
|
|
||||||
id?: number;
|
|
||||||
|
|
||||||
constructor(options: AuthInfo) {
|
|
||||||
this.ip = options.ip;
|
|
||||||
this.info = options.info;
|
|
||||||
this.type = options.type;
|
|
||||||
this.id = options.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LoginStatus {
|
|
||||||
'success',
|
|
||||||
'fail',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AuthDataType {
|
|
||||||
'loginLog' = 'loginLog',
|
|
||||||
'authToken' = 'authToken',
|
|
||||||
'notification' = 'notification',
|
|
||||||
'removeLogFrequency' = 'removeLogFrequency',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
|
|
||||||
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
|
|
||||||
ip: DataTypes.STRING,
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
info: {
|
|
||||||
type: DataTypes.JSON,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+18
-6
@@ -17,15 +17,19 @@ export class Crontab {
|
|||||||
labels?: string[];
|
labels?: string[];
|
||||||
last_running_time?: number;
|
last_running_time?: number;
|
||||||
last_execution_time?: number;
|
last_execution_time?: number;
|
||||||
|
sub_id?: number;
|
||||||
|
extra_schedules?: Array<{ schedule: string }>;
|
||||||
|
task_before?: string;
|
||||||
|
task_after?: string;
|
||||||
|
|
||||||
constructor(options: Crontab) {
|
constructor(options: Crontab) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.command = options.command;
|
this.command = options.command.trim();
|
||||||
this.schedule = options.schedule;
|
this.schedule = options.schedule;
|
||||||
this.saved = options.saved;
|
this.saved = options.saved;
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.status =
|
this.status =
|
||||||
options.status && CrontabStatus[options.status]
|
typeof options.status === 'number' && CrontabStatus[options.status]
|
||||||
? options.status
|
? options.status
|
||||||
: CrontabStatus.idle;
|
: CrontabStatus.idle;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
@@ -37,17 +41,21 @@ export class Crontab {
|
|||||||
this.labels = options.labels || [];
|
this.labels = options.labels || [];
|
||||||
this.last_running_time = options.last_running_time || 0;
|
this.last_running_time = options.last_running_time || 0;
|
||||||
this.last_execution_time = options.last_execution_time || 0;
|
this.last_execution_time = options.last_execution_time || 0;
|
||||||
|
this.sub_id = options.sub_id;
|
||||||
|
this.extra_schedules = options.extra_schedules;
|
||||||
|
this.task_before = options.task_before;
|
||||||
|
this.task_after = options.task_after;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CrontabStatus {
|
export enum CrontabStatus {
|
||||||
'running',
|
'running' = 0,
|
||||||
'idle',
|
'queued' = 0.5,
|
||||||
|
'idle' = 1,
|
||||||
'disabled',
|
'disabled',
|
||||||
'queued',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { }
|
||||||
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||||
name: {
|
name: {
|
||||||
unique: 'compositeIndex',
|
unique: 'compositeIndex',
|
||||||
@@ -72,4 +80,8 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
|||||||
labels: DataTypes.JSON,
|
labels: DataTypes.JSON,
|
||||||
last_running_time: DataTypes.NUMBER,
|
last_running_time: DataTypes.NUMBER,
|
||||||
last_execution_time: DataTypes.NUMBER,
|
last_execution_time: DataTypes.NUMBER,
|
||||||
|
sub_id: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
|
extra_schedules: DataTypes.JSON,
|
||||||
|
task_before: DataTypes.STRING,
|
||||||
|
task_after: DataTypes.STRING,
|
||||||
});
|
});
|
||||||
|
|||||||
+17
-2
@@ -1,13 +1,19 @@
|
|||||||
import { sequelize } from '.';
|
import { sequelize } from '.';
|
||||||
import { DataTypes, Model } from 'sequelize';
|
import { DataTypes, Model } from 'sequelize';
|
||||||
|
|
||||||
|
export enum CronViewType {
|
||||||
|
'系统' = 1,
|
||||||
|
'个人',
|
||||||
|
}
|
||||||
|
|
||||||
interface SortType {
|
interface SortType {
|
||||||
type: 'ASC' | 'DESC';
|
type: 'ASC' | 'DESC';
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterType {
|
interface FilterType {
|
||||||
type: 'or' | 'and';
|
property: string;
|
||||||
|
operation: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +24,8 @@ export class CrontabView {
|
|||||||
isDisabled?: 1 | 0;
|
isDisabled?: 1 | 0;
|
||||||
filters?: FilterType[];
|
filters?: FilterType[];
|
||||||
sorts?: SortType[];
|
sorts?: SortType[];
|
||||||
|
filterRelation?: 'and' | 'or';
|
||||||
|
type?: CronViewType;
|
||||||
|
|
||||||
constructor(options: CrontabView) {
|
constructor(options: CrontabView) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
@@ -26,10 +34,12 @@ export class CrontabView {
|
|||||||
this.isDisabled = options.isDisabled || 0;
|
this.isDisabled = options.isDisabled || 0;
|
||||||
this.filters = options.filters;
|
this.filters = options.filters;
|
||||||
this.sorts = options.sorts;
|
this.sorts = options.sorts;
|
||||||
|
this.filterRelation = options.filterRelation;
|
||||||
|
this.type = options.type || CronViewType.个人;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CronViewInstance
|
export interface CronViewInstance
|
||||||
extends Model<CrontabView, CrontabView>,
|
extends Model<CrontabView, CrontabView>,
|
||||||
CrontabView {}
|
CrontabView {}
|
||||||
export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
||||||
@@ -43,5 +53,10 @@ export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
|||||||
isDisabled: DataTypes.NUMBER,
|
isDisabled: DataTypes.NUMBER,
|
||||||
filters: DataTypes.JSON,
|
filters: DataTypes.JSON,
|
||||||
sorts: DataTypes.JSON,
|
sorts: DataTypes.JSON,
|
||||||
|
filterRelation: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+26
-9
@@ -4,18 +4,21 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|||||||
export class Dependence {
|
export class Dependence {
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
status?: DependenceStatus;
|
status: DependenceStatus;
|
||||||
type?: DependenceTypes;
|
type: DependenceTypes;
|
||||||
name?: number;
|
name: string;
|
||||||
log?: string[];
|
log?: string[];
|
||||||
remark?: string;
|
remark?: string;
|
||||||
|
|
||||||
constructor(options: Dependence) {
|
constructor(options: Dependence) {
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.status = options.status || DependenceStatus.installing;
|
this.status =
|
||||||
|
typeof options.status === 'number' && DependenceStatus[options.status]
|
||||||
|
? options.status
|
||||||
|
: DependenceStatus.queued;
|
||||||
this.type = options.type || DependenceTypes.nodejs;
|
this.type = options.type || DependenceTypes.nodejs;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.name = options.name;
|
this.name = options.name.trim();
|
||||||
this.log = options.log || [];
|
this.log = options.log || [];
|
||||||
this.remark = options.remark || '';
|
this.remark = options.remark || '';
|
||||||
}
|
}
|
||||||
@@ -28,6 +31,8 @@ export enum DependenceStatus {
|
|||||||
'removing',
|
'removing',
|
||||||
'removed',
|
'removed',
|
||||||
'removeFailed',
|
'removeFailed',
|
||||||
|
'queued',
|
||||||
|
'cancelled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DependenceTypes {
|
export enum DependenceTypes {
|
||||||
@@ -38,17 +43,29 @@ export enum DependenceTypes {
|
|||||||
|
|
||||||
export enum InstallDependenceCommandTypes {
|
export enum InstallDependenceCommandTypes {
|
||||||
'pnpm add -g',
|
'pnpm add -g',
|
||||||
'pip3 install',
|
'pip3 install --disable-pip-version-check --root-user-action=ignore',
|
||||||
'apk add',
|
'apk add --no-check-certificate',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GetDependenceCommandTypes {
|
||||||
|
'pnpm ls -g ',
|
||||||
|
'pip3 show --disable-pip-version-check',
|
||||||
|
'apk info -es',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum versionDependenceCommandTypes {
|
||||||
|
'@',
|
||||||
|
'==',
|
||||||
|
'=',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum unInstallDependenceCommandTypes {
|
export enum unInstallDependenceCommandTypes {
|
||||||
'pnpm remove -g',
|
'pnpm remove -g',
|
||||||
'pip3 uninstall -y',
|
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
||||||
'apk del',
|
'apk del',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DependenceInstance
|
export interface DependenceInstance
|
||||||
extends Model<Dependence, Dependence>,
|
extends Model<Dependence, Dependence>,
|
||||||
Dependence {}
|
Dependence {}
|
||||||
export const DependenceModel = sequelize.define<DependenceInstance>(
|
export const DependenceModel = sequelize.define<DependenceInstance>(
|
||||||
|
|||||||
+10
-4
@@ -13,11 +13,14 @@ export class Env {
|
|||||||
constructor(options: Env) {
|
constructor(options: Env) {
|
||||||
this.value = options.value;
|
this.value = options.value;
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.status = options.status || EnvStatus.normal;
|
this.status =
|
||||||
|
typeof options.status === 'number' && EnvStatus[options.status]
|
||||||
|
? options.status
|
||||||
|
: EnvStatus.normal;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.position = options.position;
|
this.position = options.position;
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.remarks = options.remarks;
|
this.remarks = options.remarks || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +29,12 @@ export enum EnvStatus {
|
|||||||
'disabled',
|
'disabled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initEnvPosition = 9999999999;
|
export const maxPosition = 9000000000000000;
|
||||||
|
export const initPosition = 4500000000000000;
|
||||||
|
export const stepPosition = 10000000000;
|
||||||
|
export const minPosition = 100;
|
||||||
|
|
||||||
interface EnvInstance extends Model<Env, Env>, Env {}
|
export interface EnvInstance extends Model<Env, Env>, Env {}
|
||||||
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||||
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||||
timestamp: DataTypes.STRING,
|
timestamp: DataTypes.STRING,
|
||||||
|
|||||||
+14
-6
@@ -1,15 +1,23 @@
|
|||||||
import { Sequelize } from 'sequelize';
|
import { Sequelize, Transaction } from 'sequelize';
|
||||||
import config from '../config/index';
|
import config from '../config/index';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
export const sequelize = new Sequelize({
|
export const sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: `${config.dbPath}database.sqlite`,
|
storage: join(config.dbPath, 'database.sqlite'),
|
||||||
logging: false,
|
logging: false,
|
||||||
pool: {
|
retry: {
|
||||||
max: 6,
|
max: 10,
|
||||||
min: 0,
|
match: ['SQLITE_BUSY: database is locked'],
|
||||||
idle: 30000,
|
|
||||||
},
|
},
|
||||||
|
pool: {
|
||||||
|
max: 5,
|
||||||
|
min: 2,
|
||||||
|
idle: 30000,
|
||||||
|
acquire: 30000,
|
||||||
|
evict: 10000,
|
||||||
|
},
|
||||||
|
transactionType: Transaction.TYPES.IMMEDIATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ResponseType<T> = { code: number; data?: T; message?: string };
|
export type ResponseType<T> = { code: number; data?: T; message?: string };
|
||||||
|
|||||||
+71
-4
@@ -11,10 +11,17 @@ export enum NotificationMode {
|
|||||||
'dingtalkBot' = 'dingtalkBot',
|
'dingtalkBot' = 'dingtalkBot',
|
||||||
'weWorkBot' = 'weWorkBot',
|
'weWorkBot' = 'weWorkBot',
|
||||||
'weWorkApp' = 'weWorkApp',
|
'weWorkApp' = 'weWorkApp',
|
||||||
|
'aibotk' = 'aibotk',
|
||||||
'iGot' = 'iGot',
|
'iGot' = 'iGot',
|
||||||
'pushPlus' = 'pushPlus',
|
'pushPlus' = 'pushPlus',
|
||||||
|
'wePlusBot' = 'wePlusBot',
|
||||||
'email' = 'email',
|
'email' = 'email',
|
||||||
|
'pushMe' = 'pushMe',
|
||||||
|
'feishu' = 'feishu',
|
||||||
'webhook' = 'webhook',
|
'webhook' = 'webhook',
|
||||||
|
'chronocat' = 'Chronocat',
|
||||||
|
'ntfy' = 'ntfy',
|
||||||
|
'wxPusherBot' = 'wxPusherBot',
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class NotificationBaseInfo {
|
abstract class NotificationBaseInfo {
|
||||||
@@ -39,6 +46,7 @@ export class ServerChanNotification extends NotificationBaseInfo {
|
|||||||
|
|
||||||
export class PushDeerNotification extends NotificationBaseInfo {
|
export class PushDeerNotification extends NotificationBaseInfo {
|
||||||
public pushDeerKey = '';
|
public pushDeerKey = '';
|
||||||
|
public pushDeerUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChatNotification extends NotificationBaseInfo {
|
export class ChatNotification extends NotificationBaseInfo {
|
||||||
@@ -48,9 +56,12 @@ export class ChatNotification extends NotificationBaseInfo {
|
|||||||
|
|
||||||
export class BarkNotification extends NotificationBaseInfo {
|
export class BarkNotification extends NotificationBaseInfo {
|
||||||
public barkPush = '';
|
public barkPush = '';
|
||||||
public barkIcon = 'http://qn.whyour.cn/logo.png';
|
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
||||||
public barkSound = '';
|
public barkSound = '';
|
||||||
public barkGroup = 'qinglong';
|
public barkGroup = 'qinglong';
|
||||||
|
public barkLevel = 'active';
|
||||||
|
public barkUrl = '';
|
||||||
|
public barkArchive=""
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||||
@@ -59,7 +70,7 @@ export class TelegramBotNotification extends NotificationBaseInfo {
|
|||||||
public telegramBotProxyHost = '';
|
public telegramBotProxyHost = '';
|
||||||
public telegramBotProxyPort = '';
|
public telegramBotProxyPort = '';
|
||||||
public telegramBotProxyAuth = '';
|
public telegramBotProxyAuth = '';
|
||||||
public telegramBotApiHost = 'api.telegram.org';
|
public telegramBotApiHost = 'https://api.telegram.org';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DingtalkBotNotification extends NotificationBaseInfo {
|
export class DingtalkBotNotification extends NotificationBaseInfo {
|
||||||
@@ -69,10 +80,18 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
|
|||||||
|
|
||||||
export class WeWorkBotNotification extends NotificationBaseInfo {
|
export class WeWorkBotNotification extends NotificationBaseInfo {
|
||||||
public weWorkBotKey = '';
|
public weWorkBotKey = '';
|
||||||
|
public weWorkOrigin = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WeWorkAppNotification extends NotificationBaseInfo {
|
export class WeWorkAppNotification extends NotificationBaseInfo {
|
||||||
public weWorkAppKey = '';
|
public weWorkAppKey = '';
|
||||||
|
public weWorkOrigin = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AibotkNotification extends NotificationBaseInfo {
|
||||||
|
public aibotkKey: string = '';
|
||||||
|
public aibotkType: 'room' | 'contact' = 'room';
|
||||||
|
public aibotkName: string = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IGotNotification extends NotificationBaseInfo {
|
export class IGotNotification extends NotificationBaseInfo {
|
||||||
@@ -82,6 +101,17 @@ export class IGotNotification extends NotificationBaseInfo {
|
|||||||
export class PushPlusNotification extends NotificationBaseInfo {
|
export class PushPlusNotification extends NotificationBaseInfo {
|
||||||
public pushPlusToken = '';
|
public pushPlusToken = '';
|
||||||
public pushPlusUser = '';
|
public pushPlusUser = '';
|
||||||
|
public pushPlusTemplate = '';
|
||||||
|
public pushplusChannel = '';
|
||||||
|
public pushplusWebhook = '';
|
||||||
|
public pushplusCallbackUrl = '';
|
||||||
|
public pushplusTo = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WePlusBotNotification extends NotificationBaseInfo {
|
||||||
|
public wePlusBotToken = '';
|
||||||
|
public wePlusBotReceiver = '';
|
||||||
|
public wePlusBotVersion = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EmailNotification extends NotificationBaseInfo {
|
export class EmailNotification extends NotificationBaseInfo {
|
||||||
@@ -90,12 +120,42 @@ export class EmailNotification extends NotificationBaseInfo {
|
|||||||
public emailPass: string = '';
|
public emailPass: string = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PushMeNotification extends NotificationBaseInfo {
|
||||||
|
public pushMeKey: string = '';
|
||||||
|
public pushMeUrl: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChronocatNotification extends NotificationBaseInfo {
|
||||||
|
public chronocatURL: string = '';
|
||||||
|
public chronocatQQ: string = '';
|
||||||
|
public chronocatToken: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
export class WebhookNotification extends NotificationBaseInfo {
|
export class WebhookNotification extends NotificationBaseInfo {
|
||||||
public webhookHeaders: string = '';
|
public webhookHeaders: string = '';
|
||||||
public webhookBody: string = '';
|
public webhookBody: string = '';
|
||||||
public webhookUrl: string = '';
|
public webhookUrl: string = '';
|
||||||
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
|
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
|
||||||
public webhookContentType: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded' = 'application/json';
|
public webhookContentType:
|
||||||
|
| 'application/json'
|
||||||
|
| 'multipart/form-data'
|
||||||
|
| 'application/x-www-form-urlencoded' = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LarkNotification extends NotificationBaseInfo {
|
||||||
|
public larkKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NtfyNotification extends NotificationBaseInfo {
|
||||||
|
public ntfyUrl = '';
|
||||||
|
public ntfyTopic = '';
|
||||||
|
public ntfyPriority = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WxPusherBotNotification extends NotificationBaseInfo {
|
||||||
|
public wxPusherBotAppToken = '';
|
||||||
|
public wxPusherBotTopicIds = '';
|
||||||
|
public wxPusherBotUids = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationInfo
|
export interface NotificationInfo
|
||||||
@@ -109,7 +169,14 @@ export interface NotificationInfo
|
|||||||
DingtalkBotNotification,
|
DingtalkBotNotification,
|
||||||
WeWorkBotNotification,
|
WeWorkBotNotification,
|
||||||
WeWorkAppNotification,
|
WeWorkAppNotification,
|
||||||
|
AibotkNotification,
|
||||||
IGotNotification,
|
IGotNotification,
|
||||||
PushPlusNotification,
|
PushPlusNotification,
|
||||||
|
WePlusBotNotification,
|
||||||
EmailNotification,
|
EmailNotification,
|
||||||
WebhookNotification {}
|
PushMeNotification,
|
||||||
|
WebhookNotification,
|
||||||
|
ChronocatNotification,
|
||||||
|
LarkNotification,
|
||||||
|
NtfyNotification,
|
||||||
|
WxPusherBotNotification {}
|
||||||
|
|||||||
+2
-9
@@ -24,16 +24,9 @@ export interface AppToken {
|
|||||||
expiration: number;
|
expiration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system';
|
||||||
|
|
||||||
export enum CrontabStatus {
|
export interface AppInstance extends Model<App, App>, App {}
|
||||||
'running',
|
|
||||||
'idle',
|
|
||||||
'disabled',
|
|
||||||
'queued',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AppInstance extends Model<App, App>, App {}
|
|
||||||
export const AppModel = sequelize.define<AppInstance>('App', {
|
export const AppModel = sequelize.define<AppInstance>('App', {
|
||||||
name: { type: DataTypes.STRING, unique: 'name' },
|
name: { type: DataTypes.STRING, unique: 'name' },
|
||||||
scopes: DataTypes.JSON,
|
scopes: DataTypes.JSON,
|
||||||
|
|||||||
+4
-1
@@ -16,4 +16,7 @@ export type SockMessageType =
|
|||||||
| 'uninstallDependence'
|
| 'uninstallDependence'
|
||||||
| 'updateSystemVersion'
|
| 'updateSystemVersion'
|
||||||
| 'manuallyRunScript'
|
| 'manuallyRunScript'
|
||||||
| 'runSubscriptionEnd';
|
| 'runSubscriptionEnd'
|
||||||
|
| 'reloadSystem'
|
||||||
|
| 'updateNodeMirror'
|
||||||
|
| 'updateLinuxMirror';
|
||||||
|
|||||||
@@ -28,14 +28,17 @@ export class Subscription {
|
|||||||
extensions?: string;
|
extensions?: string;
|
||||||
sub_before?: string;
|
sub_before?: string;
|
||||||
sub_after?: string;
|
sub_after?: string;
|
||||||
|
proxy?: string;
|
||||||
|
autoAddCron?: 1 | 0;
|
||||||
|
autoDelCron?: 1 | 0;
|
||||||
|
|
||||||
constructor(options: Subscription) {
|
constructor(options: Subscription) {
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.name = options.name || options.alias;
|
this.name = options.name || options.alias;
|
||||||
this.type = options.type;
|
this.type = options.type;
|
||||||
this.schedule = options.schedule;
|
this.schedule = options.schedule;
|
||||||
this.status =
|
this.status = this.status =
|
||||||
options.status && SubscriptionStatus[options.status]
|
typeof options.status === 'number' && SubscriptionStatus[options.status]
|
||||||
? options.status
|
? options.status
|
||||||
: SubscriptionStatus.idle;
|
: SubscriptionStatus.idle;
|
||||||
this.url = options.url;
|
this.url = options.url;
|
||||||
@@ -54,6 +57,9 @@ export class Subscription {
|
|||||||
this.extensions = options.extensions;
|
this.extensions = options.extensions;
|
||||||
this.sub_before = options.sub_before;
|
this.sub_before = options.sub_before;
|
||||||
this.sub_after = options.sub_after;
|
this.sub_after = options.sub_after;
|
||||||
|
this.proxy = options.proxy;
|
||||||
|
this.autoAddCron = options.autoAddCron ? 1 : 0;
|
||||||
|
this.autoDelCron = options.autoDelCron ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +70,7 @@ export enum SubscriptionStatus {
|
|||||||
'queued',
|
'queued',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubscriptionInstance
|
export interface SubscriptionInstance
|
||||||
extends Model<Subscription, Subscription>,
|
extends Model<Subscription, Subscription>,
|
||||||
Subscription {}
|
Subscription {}
|
||||||
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
||||||
@@ -102,5 +108,8 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
|||||||
log_path: DataTypes.STRING,
|
log_path: DataTypes.STRING,
|
||||||
schedule_type: DataTypes.STRING,
|
schedule_type: DataTypes.STRING,
|
||||||
alias: { type: DataTypes.STRING, unique: 'alias' },
|
alias: { type: DataTypes.STRING, unique: 'alias' },
|
||||||
|
proxy: { type: DataTypes.STRING, allowNull: true },
|
||||||
|
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
|
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { sequelize } from '.';
|
||||||
|
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||||
|
import { NotificationInfo } from './notify';
|
||||||
|
|
||||||
|
export class SystemInfo {
|
||||||
|
ip?: string;
|
||||||
|
type: AuthDataType;
|
||||||
|
info?: SystemModelInfo;
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
constructor(options: SystemInfo) {
|
||||||
|
this.ip = options.ip;
|
||||||
|
this.info = options.info;
|
||||||
|
this.type = options.type;
|
||||||
|
this.id = options.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LoginStatus {
|
||||||
|
'success',
|
||||||
|
'fail',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthDataType {
|
||||||
|
'loginLog' = 'loginLog',
|
||||||
|
'authToken' = 'authToken',
|
||||||
|
'notification' = 'notification',
|
||||||
|
'removeLogFrequency' = 'removeLogFrequency',
|
||||||
|
'systemConfig' = 'systemConfig',
|
||||||
|
'authConfig' = 'authConfig',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemConfigInfo {
|
||||||
|
logRemoveFrequency?: number;
|
||||||
|
cronConcurrency?: number;
|
||||||
|
dependenceProxy?: string;
|
||||||
|
nodeMirror?: string;
|
||||||
|
pythonMirror?: string;
|
||||||
|
linuxMirror?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginLogInfo {
|
||||||
|
timestamp?: number;
|
||||||
|
address?: string;
|
||||||
|
ip?: string;
|
||||||
|
platform?: string;
|
||||||
|
status?: LoginStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthInfo {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
retries: number;
|
||||||
|
lastlogon: number;
|
||||||
|
lastip: string;
|
||||||
|
lastaddr: string;
|
||||||
|
platform: string;
|
||||||
|
isTwoFactorChecking: boolean;
|
||||||
|
token: string;
|
||||||
|
tokens: Record<string, string>;
|
||||||
|
twoFactorActivated: boolean;
|
||||||
|
twoFactorSecret: string;
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SystemModelInfo = SystemConfigInfo &
|
||||||
|
Partial<NotificationInfo> &
|
||||||
|
LoginLogInfo &
|
||||||
|
Partial<AuthInfo>;
|
||||||
|
|
||||||
|
export interface SystemInstance
|
||||||
|
extends Model<SystemInfo, SystemInfo>,
|
||||||
|
SystemInfo {}
|
||||||
|
export const SystemModel = sequelize.define<SystemInstance>('Auth', {
|
||||||
|
ip: DataTypes.STRING,
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
info: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Vendored
+2
@@ -3,3 +3,5 @@ declare namespace Express {
|
|||||||
platform: 'desktop' | 'mobile';
|
platform: 'desktop' | 'mobile';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'pstree.remy';
|
||||||
|
|||||||
+13
-8
@@ -7,18 +7,23 @@ import linkDeps from './deps';
|
|||||||
import initTask from './initTask';
|
import initTask from './initTask';
|
||||||
|
|
||||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||||
await depInjectorLoader();
|
depInjectorLoader();
|
||||||
Logger.info('✌️ Dependency Injector loaded');
|
Logger.info('✌️ Dependency loaded');
|
||||||
|
console.log('✌️ Dependency loaded');
|
||||||
await expressLoader({ app: expressApp });
|
|
||||||
Logger.info('✌️ Express loaded');
|
|
||||||
|
|
||||||
await initData();
|
await initData();
|
||||||
Logger.info('✌️ init data loaded');
|
Logger.info('✌️ Init data loaded');
|
||||||
|
console.log('✌️ Init data loaded');
|
||||||
|
|
||||||
await linkDeps();
|
await linkDeps();
|
||||||
Logger.info('✌️ link deps loaded');
|
Logger.info('✌️ Link deps loaded');
|
||||||
|
console.log('✌️ Link deps loaded');
|
||||||
|
|
||||||
initTask();
|
initTask();
|
||||||
Logger.info('✌️ init task loaded');
|
Logger.info('✌️ Init task loaded');
|
||||||
|
console.log('✌️ Init task loaded');
|
||||||
|
|
||||||
|
expressLoader({ app: expressApp });
|
||||||
|
Logger.info('✌️ Express loaded');
|
||||||
|
console.log('✌️ Express loaded');
|
||||||
};
|
};
|
||||||
|
|||||||
+44
-86
@@ -1,108 +1,66 @@
|
|||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import path from 'path';
|
|
||||||
import DataStore from 'nedb';
|
|
||||||
import { EnvModel } from '../data/env';
|
import { EnvModel } from '../data/env';
|
||||||
import { CrontabModel } from '../data/cron';
|
import { CrontabModel } from '../data/cron';
|
||||||
import { DependenceModel } from '../data/dependence';
|
import { DependenceModel } from '../data/dependence';
|
||||||
import { AppModel } from '../data/open';
|
import { AppModel } from '../data/open';
|
||||||
import { AuthModel } from '../data/auth';
|
import { SystemModel } from '../data/system';
|
||||||
import { fileExist } from '../config/util';
|
|
||||||
import { SubscriptionModel } from '../data/subscription';
|
import { SubscriptionModel } from '../data/subscription';
|
||||||
import { CrontabViewModel } from '../data/cronView';
|
import { CrontabViewModel } from '../data/cronView';
|
||||||
import config from '../config';
|
import { sequelize } from '../data';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
try {
|
try {
|
||||||
await CrontabModel.sync();
|
await CrontabModel.sync();
|
||||||
await DependenceModel.sync();
|
await DependenceModel.sync();
|
||||||
await AppModel.sync();
|
await AppModel.sync();
|
||||||
await AuthModel.sync();
|
await SystemModel.sync();
|
||||||
await EnvModel.sync();
|
await EnvModel.sync();
|
||||||
await SubscriptionModel.sync();
|
await SubscriptionModel.sync();
|
||||||
await CrontabViewModel.sync();
|
await CrontabViewModel.sync();
|
||||||
|
|
||||||
// try {
|
// 初始化新增字段
|
||||||
// const queryInterface = sequelize.getQueryInterface();
|
try {
|
||||||
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
|
await sequelize.query(
|
||||||
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
|
'alter table CrontabViews add column filterRelation VARCHAR(255)',
|
||||||
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
|
);
|
||||||
// } catch (error) { }
|
} catch (error) {}
|
||||||
|
try {
|
||||||
// 2.10-2.11 升级
|
await sequelize.query(
|
||||||
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
'alter table Subscriptions add column proxy VARCHAR(255)',
|
||||||
const envDbFile = path.join(config.rootPath, 'db/env.db');
|
);
|
||||||
const appDbFile = path.join(config.rootPath, 'db/app.db');
|
} catch (error) {}
|
||||||
const authDbFile = path.join(config.rootPath, 'db/auth.db');
|
try {
|
||||||
const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
|
await sequelize.query('alter table CrontabViews add column type NUMBER');
|
||||||
const crondbExist = await fileExist(cronDbFile);
|
} catch (error) {}
|
||||||
const dependenceDbExist = await fileExist(dependenceDbFile);
|
try {
|
||||||
const envDbExist = await fileExist(envDbFile);
|
await sequelize.query(
|
||||||
const appDbExist = await fileExist(appDbFile);
|
'alter table Subscriptions add column autoAddCron NUMBER',
|
||||||
const authDbExist = await fileExist(authDbFile);
|
);
|
||||||
|
} catch (error) {}
|
||||||
const cronCount = await CrontabModel.count();
|
try {
|
||||||
const dependenceCount = await DependenceModel.count();
|
await sequelize.query(
|
||||||
const envCount = await EnvModel.count();
|
'alter table Subscriptions add column autoDelCron NUMBER',
|
||||||
const appCount = await AppModel.count();
|
);
|
||||||
const authCount = await AuthModel.count();
|
} catch (error) {}
|
||||||
if (crondbExist && cronCount === 0) {
|
try {
|
||||||
const cronDb = new DataStore({
|
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
|
||||||
filename: cronDbFile,
|
} catch (error) {}
|
||||||
autoload: true,
|
try {
|
||||||
});
|
await sequelize.query(
|
||||||
cronDb.persistence.compactDatafile();
|
'alter table Crontabs add column extra_schedules JSON',
|
||||||
cronDb.find({}).exec(async (err, docs) => {
|
);
|
||||||
await CrontabModel.bulkCreate(docs, { ignoreDuplicates: true });
|
} catch (error) {}
|
||||||
});
|
try {
|
||||||
}
|
await sequelize.query('alter table Crontabs add column task_before TEXT');
|
||||||
|
} catch (error) {}
|
||||||
if (dependenceDbExist && dependenceCount === 0) {
|
try {
|
||||||
const dependenceDb = new DataStore({
|
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
||||||
filename: dependenceDbFile,
|
} catch (error) {}
|
||||||
autoload: true,
|
|
||||||
});
|
|
||||||
dependenceDb.persistence.compactDatafile();
|
|
||||||
dependenceDb.find({}).exec(async (err, docs) => {
|
|
||||||
await DependenceModel.bulkCreate(docs, { ignoreDuplicates: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (envDbExist && envCount === 0) {
|
|
||||||
const envDb = new DataStore({
|
|
||||||
filename: envDbFile,
|
|
||||||
autoload: true,
|
|
||||||
});
|
|
||||||
envDb.persistence.compactDatafile();
|
|
||||||
envDb.find({}).exec(async (err, docs) => {
|
|
||||||
await EnvModel.bulkCreate(docs, { ignoreDuplicates: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appDbExist && appCount === 0) {
|
|
||||||
const appDb = new DataStore({
|
|
||||||
filename: appDbFile,
|
|
||||||
autoload: true,
|
|
||||||
});
|
|
||||||
appDb.persistence.compactDatafile();
|
|
||||||
appDb.find({}).exec(async (err, docs) => {
|
|
||||||
await AppModel.bulkCreate(docs, { ignoreDuplicates: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authDbExist && authCount === 0) {
|
|
||||||
const authDb = new DataStore({
|
|
||||||
filename: authDbFile,
|
|
||||||
autoload: true,
|
|
||||||
});
|
|
||||||
authDb.persistence.compactDatafile();
|
|
||||||
authDb.find({}).exec(async (err, docs) => {
|
|
||||||
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
console.log('✌️ DB loaded');
|
||||||
Logger.info('✌️ DB loaded');
|
Logger.info('✌️ DB loaded');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.info('✌️ DB load failed');
|
console.error('✌️ DB load failed');
|
||||||
Logger.info(error);
|
Logger.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-14
@@ -1,20 +1,19 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs/promises';
|
||||||
import chokidar from 'chokidar';
|
import chokidar from 'chokidar';
|
||||||
import config from '../config/index';
|
import config from '../config/index';
|
||||||
import { promiseExec } from '../config/util';
|
import { fileExist, promiseExec, rmPath } from '../config/util';
|
||||||
|
|
||||||
function linkToNodeModule(src: string, dst?: string) {
|
async function linkToNodeModule(src: string, dst?: string) {
|
||||||
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
||||||
const source = path.join(config.rootPath, src);
|
const source = path.join(config.rootPath, src);
|
||||||
|
|
||||||
fs.lstat(target, (err, stat) => {
|
try {
|
||||||
if (!stat) {
|
const stats = await fs.lstat(target);
|
||||||
fs.symlink(source, target, 'dir', (err) => {
|
if (!stats) {
|
||||||
if (err) throw err;
|
await fs.symlink(source, target, 'dir');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
} catch (error) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function linkCommand() {
|
async function linkCommand() {
|
||||||
@@ -24,26 +23,27 @@ async function linkCommand() {
|
|||||||
{
|
{
|
||||||
src: 'update.sh',
|
src: 'update.sh',
|
||||||
dest: 'ql',
|
dest: 'ql',
|
||||||
|
tmp: 'ql_tmp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'task.sh',
|
src: 'task.sh',
|
||||||
dest: 'task',
|
dest: 'task',
|
||||||
|
tmp: 'task_tmp',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const link of linkShell) {
|
for (const link of linkShell) {
|
||||||
const source = path.join(config.rootPath, 'shell', link.src);
|
const source = path.join(config.rootPath, 'shell', link.src);
|
||||||
const target = path.join(commandDir, link.dest);
|
const target = path.join(commandDir, link.dest);
|
||||||
if (fs.existsSync(target)) {
|
const tmpTarget = path.join(commandDir, link.tmp);
|
||||||
fs.unlinkSync(target);
|
await fs.symlink(source, tmpTarget);
|
||||||
}
|
await fs.rename(tmpTarget, target);
|
||||||
fs.symlink(source, target, (err) => {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (src: string = 'deps') => {
|
export default async (src: string = 'deps') => {
|
||||||
await linkCommand();
|
await linkCommand();
|
||||||
linkToNodeModule(src);
|
await linkToNodeModule(src);
|
||||||
|
|
||||||
const source = path.join(config.rootPath, src);
|
const source = path.join(config.rootPath, src);
|
||||||
const watcher = chokidar.watch(source, {
|
const watcher = chokidar.watch(source, {
|
||||||
|
|||||||
+31
-47
@@ -3,39 +3,37 @@ import bodyParser from 'body-parser';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import routes from '../api';
|
import routes from '../api';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import jwt, { UnauthorizedError } from 'express-jwt';
|
import { UnauthorizedError, expressjwt } from 'express-jwt';
|
||||||
import fs from 'fs';
|
|
||||||
import { getPlatform, getToken } from '../config/util';
|
import { getPlatform, getToken } from '../config/util';
|
||||||
import Container from 'typedi';
|
|
||||||
import OpenService from '../services/open';
|
|
||||||
import rewrite from 'express-urlrewrite';
|
import rewrite from 'express-urlrewrite';
|
||||||
import UserService from '../services/user';
|
|
||||||
import handler from 'serve-handler';
|
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { EnvModel } from '../data/env';
|
|
||||||
import { errors } from 'celebrate';
|
import { errors } from 'celebrate';
|
||||||
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||||
|
import { serveEnv } from '../config/serverEnv';
|
||||||
|
import Logger from './logger';
|
||||||
|
import { IKeyvStore, shareStore } from '../shared/store';
|
||||||
|
|
||||||
export default ({ app }: { app: Application }) => {
|
export default ({ app }: { app: Application }) => {
|
||||||
app.enable('trust proxy');
|
app.set('trust proxy', 'loopback');
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
app.get(`${config.api.prefix}/env.js`, serveEnv);
|
||||||
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use(
|
||||||
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
'/api/public',
|
||||||
next();
|
createProxyMiddleware({
|
||||||
} else {
|
target: `http://0.0.0.0:${config.publicPort}/api`,
|
||||||
return handler(req, res, {
|
changeOrigin: true,
|
||||||
public: 'static/dist',
|
pathRewrite: { '/api/public': '' },
|
||||||
rewrites: [{ source: '**', destination: '/index.html' }],
|
logger: Logger,
|
||||||
});
|
}),
|
||||||
}
|
);
|
||||||
});
|
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
jwt({
|
expressjwt({
|
||||||
secret: config.secret as string,
|
secret: config.secret,
|
||||||
algorithms: ['HS384'],
|
algorithms: ['HS384'],
|
||||||
}).unless({
|
}).unless({
|
||||||
path: [...config.apiWhiteList, /^\/open\//],
|
path: [...config.apiWhiteList, /^\/open\//],
|
||||||
@@ -55,8 +53,10 @@ export default ({ app }: { app: Application }) => {
|
|||||||
app.use(async (req, res, next) => {
|
app.use(async (req, res, next) => {
|
||||||
const headerToken = getToken(req);
|
const headerToken = getToken(req);
|
||||||
if (req.path.startsWith('/open/')) {
|
if (req.path.startsWith('/open/')) {
|
||||||
const openService = Container.get(OpenService);
|
const apps = await shareStore.getApps();
|
||||||
const doc = await openService.findTokenByValue(headerToken);
|
const doc = apps?.filter((x) =>
|
||||||
|
x.tokens?.find((y) => y.value === headerToken),
|
||||||
|
)?.[0];
|
||||||
if (doc && doc.tokens && doc.tokens.length > 0) {
|
if (doc && doc.tokens && doc.tokens.length > 0) {
|
||||||
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
||||||
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
||||||
@@ -80,9 +80,9 @@ export default ({ app }: { app: Application }) => {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
const authInfo = await shareStore.getAuthInfo();
|
||||||
if (data && headerToken) {
|
if (authInfo && headerToken) {
|
||||||
const { token = '', tokens = {} } = JSON.parse(data);
|
const { token = '', tokens = {} } = authInfo;
|
||||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
@@ -100,16 +100,14 @@ export default ({ app }: { app: Application }) => {
|
|||||||
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const userService = Container.get(UserService);
|
const authInfo =
|
||||||
const authInfo = await userService.getUserInfo();
|
(await shareStore.getAuthInfo()) || ({} as IKeyvStore['authInfo']);
|
||||||
const envCount = await EnvModel.count();
|
|
||||||
|
|
||||||
let isInitialized = true;
|
let isInitialized = true;
|
||||||
if (
|
if (
|
||||||
Object.keys(authInfo).length === 2 &&
|
Object.keys(authInfo).length === 2 &&
|
||||||
authInfo.username === 'admin' &&
|
authInfo.username === 'admin' &&
|
||||||
authInfo.password === 'admin' &&
|
authInfo.password === 'admin'
|
||||||
envCount === 0
|
|
||||||
) {
|
) {
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
}
|
}
|
||||||
@@ -161,8 +159,8 @@ export default ({ app }: { app: Application }) => {
|
|||||||
.status(500)
|
.status(500)
|
||||||
.send({
|
.send({
|
||||||
code: 400,
|
code: 400,
|
||||||
message: `${err.name} ${err.message}`,
|
message: `${err.message}`,
|
||||||
validation: err.errors,
|
errors: err.errors,
|
||||||
})
|
})
|
||||||
.end();
|
.end();
|
||||||
}
|
}
|
||||||
@@ -170,18 +168,6 @@ export default ({ app }: { app: Application }) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(
|
|
||||||
Sentry.Handlers.errorHandler({
|
|
||||||
shouldHandleError(error) {
|
|
||||||
// 排除 SequelizeUniqueConstraintError / NotFound
|
|
||||||
return (
|
|
||||||
!['SequelizeUniqueConstraintError'].includes(error.name) ||
|
|
||||||
!['Not Found'].includes(error.message)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
err: Error & { status: number },
|
err: Error & { status: number },
|
||||||
@@ -189,8 +175,6 @@ export default ({ app }: { app: Application }) => {
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
) => {
|
) => {
|
||||||
Sentry.captureException(err);
|
|
||||||
|
|
||||||
res.status(err.status || 500);
|
res.status(err.status || 500);
|
||||||
res.json({
|
res.json({
|
||||||
code: err.status || 500,
|
code: err.status || 500,
|
||||||
|
|||||||
+122
-22
@@ -4,38 +4,131 @@ import { Container } from 'typedi';
|
|||||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
import CronService from '../services/cron';
|
import CronService from '../services/cron';
|
||||||
import EnvService from '../services/env';
|
import EnvService from '../services/env';
|
||||||
import _ from 'lodash';
|
import { DependenceModel, DependenceStatus } from '../data/dependence';
|
||||||
import { DependenceModel } from '../data/dependence';
|
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { CrontabViewModel, CronViewType } from '../data/cronView';
|
||||||
|
import { initPosition } from '../data/env';
|
||||||
|
import { AuthDataType, SystemModel } from '../data/system';
|
||||||
|
import SystemService from '../services/system';
|
||||||
|
import UserService from '../services/user';
|
||||||
|
import { writeFile, readFile } from 'fs/promises';
|
||||||
|
import { createRandomString, safeJSONParse } from '../config/util';
|
||||||
|
import OpenService from '../services/open';
|
||||||
|
import { shareStore } from '../shared/store';
|
||||||
|
import Logger from './logger';
|
||||||
|
import { AppModel } from '../data/open';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
|
||||||
// 初始化更新所有任务状态为空闲
|
// 初始化增加系统配置
|
||||||
await CrontabModel.update(
|
let systemApp = (
|
||||||
{ status: CrontabStatus.idle },
|
await AppModel.findOne({
|
||||||
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
|
where: { name: 'system' },
|
||||||
);
|
})
|
||||||
|
)?.get({ plain: true });
|
||||||
|
if (!systemApp) {
|
||||||
|
systemApp = await AppModel.create({
|
||||||
|
name: 'system',
|
||||||
|
scopes: ['crons', 'system'],
|
||||||
|
client_id: createRandomString(12, 12),
|
||||||
|
client_secret: createRandomString(24, 24),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const [systemConfig] = await SystemModel.findOrCreate({
|
||||||
|
where: { type: AuthDataType.systemConfig },
|
||||||
|
});
|
||||||
|
await SystemModel.findOrCreate({
|
||||||
|
where: { type: AuthDataType.notification },
|
||||||
|
});
|
||||||
|
const [authConfig] = await SystemModel.findOrCreate({
|
||||||
|
where: { type: AuthDataType.authConfig },
|
||||||
|
});
|
||||||
|
if (!authConfig?.info) {
|
||||||
|
let authInfo = {
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const content = await readFile(config.authConfigFile, 'utf8');
|
||||||
|
authInfo = safeJSONParse(content);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.warn('Failed to read auth config file, using default credentials');
|
||||||
|
}
|
||||||
|
await SystemModel.upsert({
|
||||||
|
id: authConfig?.id,
|
||||||
|
info: authInfo,
|
||||||
|
type: AuthDataType.authConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
const installDependencies = () => {
|
||||||
DependenceModel.findAll({
|
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||||
where: {},
|
DependenceModel.findAll({
|
||||||
order: [['type', 'DESC']],
|
where: {},
|
||||||
|
order: [
|
||||||
|
['type', 'DESC'],
|
||||||
|
['createdAt', 'DESC'],
|
||||||
|
],
|
||||||
|
raw: true,
|
||||||
|
}).then(async (docs) => {
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.queued, log: [] },
|
||||||
|
{ where: { id: docs.map((x) => x.id!) } },
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
dependenceService.installDependenceOneByOne(docs);
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化更新 linux/python/nodejs 镜像源配置
|
||||||
|
if (systemConfig.info?.pythonMirror) {
|
||||||
|
systemService.updatePythonMirror({
|
||||||
|
pythonMirror: systemConfig.info?.pythonMirror,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (systemConfig.info?.linuxMirror) {
|
||||||
|
systemService.updateLinuxMirror(
|
||||||
|
{
|
||||||
|
linuxMirror: systemConfig.info?.linuxMirror,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
() => installDependencies(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
installDependencies();
|
||||||
|
}
|
||||||
|
if (systemConfig.info?.nodeMirror) {
|
||||||
|
systemService.updateNodeMirror({
|
||||||
|
nodeMirror: systemConfig.info?.nodeMirror,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化新增默认全部任务视图
|
||||||
|
CrontabViewModel.findAll({
|
||||||
|
where: { type: CronViewType.系统, name: '全部任务' },
|
||||||
raw: true,
|
raw: true,
|
||||||
}).then(async (docs) => {
|
}).then((docs) => {
|
||||||
const groups = _.groupBy(docs, 'type');
|
if (docs.length === 0) {
|
||||||
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
|
CrontabViewModel.create({
|
||||||
for (const key of keys) {
|
name: '全部任务',
|
||||||
const group = groups[key];
|
type: CronViewType.系统,
|
||||||
const depIds = group.map((x) => x.id);
|
position: initPosition / 2,
|
||||||
await dependenceService.reInstall(depIds as number[]);
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化时执行一次所有的ql repo 任务
|
// 初始化更新所有任务状态为空闲
|
||||||
|
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
|
||||||
|
|
||||||
|
// 初始化时执行一次所有的 ql repo 任务
|
||||||
CrontabModel.findAll({
|
CrontabModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
isDisabled: { [Op.ne]: 1 },
|
isDisabled: { [Op.ne]: 1 },
|
||||||
@@ -77,7 +170,7 @@ export default async () => {
|
|||||||
if (doc.command.includes(`${config.rootPath}/log/`)) {
|
if (doc.command.includes(`${config.rootPath}/log/`)) {
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{
|
{
|
||||||
command: `${config.rootPath}/data/log/${doc.command.replace(
|
command: `${config.dataPath}/log/${doc.command.replace(
|
||||||
`${config.rootPath}/log/`,
|
`${config.rootPath}/log/`,
|
||||||
'',
|
'',
|
||||||
)}`,
|
)}`,
|
||||||
@@ -88,7 +181,7 @@ export default async () => {
|
|||||||
if (doc.command.includes(`${config.rootPath}/config/`)) {
|
if (doc.command.includes(`${config.rootPath}/config/`)) {
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{
|
{
|
||||||
command: `${config.rootPath}/data/config/${doc.command.replace(
|
command: `${config.dataPath}/config/${doc.command.replace(
|
||||||
`${config.rootPath}/config/`,
|
`${config.rootPath}/config/`,
|
||||||
'',
|
'',
|
||||||
)}`,
|
)}`,
|
||||||
@@ -99,7 +192,7 @@ export default async () => {
|
|||||||
if (doc.command.includes(`${config.rootPath}/db/`)) {
|
if (doc.command.includes(`${config.rootPath}/db/`)) {
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{
|
{
|
||||||
command: `${config.rootPath}/data/db/${doc.command.replace(
|
command: `${config.dataPath}/db/${doc.command.replace(
|
||||||
`${config.rootPath}/db/`,
|
`${config.rootPath}/db/`,
|
||||||
'',
|
'',
|
||||||
)}`,
|
)}`,
|
||||||
@@ -114,4 +207,11 @@ export default async () => {
|
|||||||
// 初始化保存一次ck和定时任务数据
|
// 初始化保存一次ck和定时任务数据
|
||||||
await cronService.autosave_crontab();
|
await cronService.autosave_crontab();
|
||||||
await envService.set_envs();
|
await envService.set_envs();
|
||||||
|
|
||||||
|
const authInfo = await userService.getAuthInfo();
|
||||||
|
const apps = await openService.findApps();
|
||||||
|
await shareStore.updateAuthInfo(authInfo);
|
||||||
|
if (apps?.length) {
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+103
-45
@@ -1,68 +1,126 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import dotenv from 'dotenv';
|
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { fileExist } from '../config/util';
|
import { fileExist } from '../config/util';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
const rootPath = process.env.QL_DIR as string;;
|
const rootPath = process.env.QL_DIR as string;
|
||||||
const dataPath = path.join(rootPath, 'data/');
|
let dataPath = path.join(rootPath, 'data/');
|
||||||
|
|
||||||
|
if (process.env.QL_DATA_DIR) {
|
||||||
|
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const preloadPath = path.join(rootPath, 'shell/preload/');
|
||||||
const configPath = path.join(dataPath, 'config/');
|
const configPath = path.join(dataPath, 'config/');
|
||||||
const scriptPath = path.join(dataPath, 'scripts/');
|
const scriptPath = path.join(dataPath, 'scripts/');
|
||||||
const logPath = path.join(dataPath, 'log/');
|
const logPath = path.join(dataPath, 'log/');
|
||||||
const uploadPath = path.join(dataPath, 'upload/');
|
const uploadPath = path.join(dataPath, 'upload/');
|
||||||
const bakPath = path.join(dataPath, 'bak/');
|
const bakPath = path.join(dataPath, 'bak/');
|
||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
|
const tmpPath = path.join(logPath, '.tmp/');
|
||||||
const confFile = path.join(configPath, 'config.sh');
|
const confFile = path.join(configPath, 'config.sh');
|
||||||
const authConfigFile = path.join(configPath, 'auth.json');
|
|
||||||
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
||||||
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
|
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
|
||||||
|
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
|
||||||
|
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
|
||||||
|
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
|
||||||
|
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
|
||||||
|
const jsNotifyFile = path.join(preloadPath, 'notify.js');
|
||||||
|
const pyNotifyFile = path.join(preloadPath, 'notify.py');
|
||||||
|
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
|
||||||
|
const TaskBeforeJsFile = path.join(configPath, 'task_before.js');
|
||||||
|
const TaskBeforePyFile = path.join(configPath, 'task_before.py');
|
||||||
|
const TaskAfterFile = path.join(configPath, 'task_after.sh');
|
||||||
const homedir = os.homedir();
|
const homedir = os.homedir();
|
||||||
const sshPath = path.resolve(homedir, '.ssh');
|
const sshPath = path.resolve(homedir, '.ssh');
|
||||||
|
const sshdPath = path.join(dataPath, 'ssh.d');
|
||||||
|
const systemLogPath = path.join(dataPath, 'syslog');
|
||||||
|
|
||||||
|
const directories = [
|
||||||
|
configPath,
|
||||||
|
scriptPath,
|
||||||
|
preloadPath,
|
||||||
|
logPath,
|
||||||
|
tmpPath,
|
||||||
|
uploadPath,
|
||||||
|
sshPath,
|
||||||
|
bakPath,
|
||||||
|
sshdPath,
|
||||||
|
systemLogPath,
|
||||||
|
];
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
target: confFile,
|
||||||
|
source: sampleConfigFile,
|
||||||
|
checkExistence: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: jsNotifyFile,
|
||||||
|
source: sampleNotifyJsFile,
|
||||||
|
checkExistence: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: pyNotifyFile,
|
||||||
|
source: sampleNotifyPyFile,
|
||||||
|
checkExistence: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: scriptNotifyJsFile,
|
||||||
|
source: sampleNotifyJsFile,
|
||||||
|
checkExistence: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: scriptNotifyPyFile,
|
||||||
|
source: sampleNotifyPyFile,
|
||||||
|
checkExistence: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: TaskBeforeFile,
|
||||||
|
source: sampleTaskShellFile,
|
||||||
|
checkExistence: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: TaskBeforeJsFile,
|
||||||
|
content:
|
||||||
|
'// The JavaScript code that executes before the JavaScript task execution will execute.',
|
||||||
|
checkExistence: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: TaskBeforePyFile,
|
||||||
|
content:
|
||||||
|
'# The Python code that executes before the Python task execution will execute.',
|
||||||
|
checkExistence: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: TaskAfterFile,
|
||||||
|
source: sampleTaskShellFile,
|
||||||
|
checkExistence: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const authFileExist = await fileExist(authConfigFile);
|
for (const dirPath of directories) {
|
||||||
const confFileExist = await fileExist(confFile);
|
if (!(await fileExist(dirPath))) {
|
||||||
const scriptDirExist = await fileExist(scriptPath);
|
await fs.mkdir(dirPath);
|
||||||
const logDirExist = await fileExist(logPath);
|
}
|
||||||
const configDirExist = await fileExist(configPath);
|
|
||||||
const uploadDirExist = await fileExist(uploadPath);
|
|
||||||
const sshDirExist = await fileExist(sshPath);
|
|
||||||
const bakDirExist = await fileExist(bakPath);
|
|
||||||
|
|
||||||
if (!configDirExist) {
|
|
||||||
fs.mkdirSync(configPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authFileExist) {
|
for (const item of files) {
|
||||||
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
const exists = await fileExist(item.target);
|
||||||
|
if (!item.checkExistence || !exists) {
|
||||||
|
if (!item.content && !item.source) {
|
||||||
|
throw new Error(
|
||||||
|
`Neither content nor source specified for ${item.target}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const content = item.content || (await fs.readFile(item.source!));
|
||||||
|
await writeFileWithLock(item.target, content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!confFileExist) {
|
|
||||||
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!scriptDirExist) {
|
|
||||||
fs.mkdirSync(scriptPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!logDirExist) {
|
|
||||||
fs.mkdirSync(logPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!uploadDirExist) {
|
|
||||||
fs.mkdirSync(uploadPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sshDirExist) {
|
|
||||||
fs.mkdirSync(sshPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bakDirExist) {
|
|
||||||
fs.mkdirSync(bakPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
dotenv.config({ path: confFile });
|
|
||||||
|
|
||||||
Logger.info('✌️ Init file down');
|
Logger.info('✌️ Init file down');
|
||||||
|
console.log('✌️ Init file down');
|
||||||
};
|
};
|
||||||
|
|||||||
+36
-25
@@ -1,10 +1,10 @@
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import _ from 'lodash';
|
|
||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
import ScheduleService from '../services/schedule';
|
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
||||||
import SubscriptionService from '../services/subscription';
|
import SubscriptionService from '../services/subscription';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { fileExist } from '../config/util';
|
import { fileExist } from '../config/util';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
@@ -12,8 +12,12 @@ export default async () => {
|
|||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
|
||||||
// 生成内置token
|
// 生成内置token
|
||||||
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
|
let tokenCommand = `ts-node-transpile-only ${join(
|
||||||
const tokenFile = `${config.rootPath}static/build/token.js`;
|
config.rootPath,
|
||||||
|
'back/token.ts',
|
||||||
|
)}`;
|
||||||
|
const tokenFile = join(config.rootPath, 'static/build/token.js');
|
||||||
|
|
||||||
if (await fileExist(tokenFile)) {
|
if (await fileExist(tokenFile)) {
|
||||||
tokenCommand = `node ${tokenFile}`;
|
tokenCommand = `node ${tokenFile}`;
|
||||||
}
|
}
|
||||||
@@ -21,32 +25,39 @@ export default async () => {
|
|||||||
id: NaN,
|
id: NaN,
|
||||||
name: '生成token',
|
name: '生成token',
|
||||||
command: tokenCommand,
|
command: tokenCommand,
|
||||||
};
|
runOrigin: 'system',
|
||||||
scheduleService.createIntervalTask(cron, {
|
} as ScheduleTaskType;
|
||||||
days: 28,
|
await scheduleService.cancelIntervalTask(cron);
|
||||||
});
|
scheduleService.createIntervalTask(
|
||||||
|
cron,
|
||||||
|
{
|
||||||
|
days: 28,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
// 运行删除日志任务
|
// 运行删除日志任务
|
||||||
const data = await systemService.getLogRemoveFrequency();
|
const data = await systemService.getSystemConfig();
|
||||||
if (data && data.info && data.info.frequency) {
|
if (data && data.info && data.info.logRemoveFrequency) {
|
||||||
const cron = {
|
const rmlogCron = {
|
||||||
id: data.id,
|
id: data.id as number,
|
||||||
name: '删除日志',
|
name: '删除日志',
|
||||||
command: `ql rmlog ${data.info.frequency}`,
|
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||||
|
runOrigin: 'system' as const,
|
||||||
};
|
};
|
||||||
scheduleService.createIntervalTask(cron, {
|
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||||
days: data.info.frequency,
|
scheduleService.createIntervalTask(
|
||||||
});
|
rmlogCron,
|
||||||
}
|
{
|
||||||
|
days: data.info.logRemoveFrequency,
|
||||||
// 运行所有订阅
|
},
|
||||||
const subs = await subscriptionService.list();
|
|
||||||
for (const sub of subs) {
|
|
||||||
await subscriptionService.handleTask(
|
|
||||||
sub,
|
|
||||||
!sub.is_disabled,
|
|
||||||
true,
|
true,
|
||||||
!sub.is_disabled,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await subscriptionService.setSshConfig();
|
||||||
|
const subs = await subscriptionService.list();
|
||||||
|
for (const sub of subs) {
|
||||||
|
subscriptionService.handleTask(sub.get({ plain: true }), !sub.is_disabled);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-21
@@ -1,32 +1,38 @@
|
|||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
|
import 'winston-daily-rotate-file';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
const transports = [];
|
const levelMap: Record<string, string> = {
|
||||||
if (process.env.NODE_ENV !== 'development') {
|
info: '\ue6f5',
|
||||||
transports.push(new winston.transports.Console());
|
warn: '\ue880',
|
||||||
} else {
|
error: '\ue602',
|
||||||
transports.push(
|
debug: '\ue67f'
|
||||||
new winston.transports.Console({
|
|
||||||
format: winston.format.combine(
|
|
||||||
winston.format.cli(),
|
|
||||||
winston.format.splat(),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customFormat = winston.format.combine(
|
||||||
|
winston.format.splat(),
|
||||||
|
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
|
winston.format.align(),
|
||||||
|
winston.format.printf((i) => `[${levelMap[i.level]}${i.level}] [${[i.timestamp]}]: ${i.message}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
format: customFormat,
|
||||||
|
datePattern: "YYYY-MM-DD",
|
||||||
|
maxSize: "20m",
|
||||||
|
maxFiles: "7d",
|
||||||
|
};
|
||||||
|
|
||||||
const LoggerInstance = winston.createLogger({
|
const LoggerInstance = winston.createLogger({
|
||||||
level: config.logs.level,
|
level: config.logs.level,
|
||||||
levels: winston.config.npm.levels,
|
levels: winston.config.npm.levels,
|
||||||
format: winston.format.combine(
|
transports: [
|
||||||
winston.format.timestamp({
|
new winston.transports.DailyRotateFile({
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
filename: path.join(config.systemLogPath, '%DATE%.log'),
|
||||||
}),
|
...defaultOptions,
|
||||||
winston.format.errors({ stack: true }),
|
})
|
||||||
winston.format.splat(),
|
],
|
||||||
winston.format.json(),
|
|
||||||
),
|
|
||||||
transports,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LoggerInstance;
|
export default LoggerInstance;
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Application } from 'express';
|
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import * as Tracing from '@sentry/tracing';
|
|
||||||
import Logger from './logger';
|
|
||||||
import config from '../config';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
export default ({ expressApp }: { expressApp: Application }) => {
|
|
||||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
|
||||||
|
|
||||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
|
||||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
|
||||||
integrations: [
|
|
||||||
new Sentry.Integrations.Http({ tracing: true }),
|
|
||||||
new Tracing.Integrations.Express({ app: expressApp }),
|
|
||||||
],
|
|
||||||
tracesSampleRate: 0.1,
|
|
||||||
release: currentVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
expressApp.use(Sentry.Handlers.requestHandler());
|
|
||||||
expressApp.use(Sentry.Handlers.tracingHandler());
|
|
||||||
|
|
||||||
Logger.info('✌️ Sentry loaded');
|
|
||||||
};
|
|
||||||
@@ -6,11 +6,11 @@ export default async ({ server }: { server: Server }) => {
|
|||||||
await Sock({ server });
|
await Sock({ server });
|
||||||
Logger.info('✌️ Sock loaded');
|
Logger.info('✌️ Sock loaded');
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('uncaughtException', (error) => {
|
||||||
Logger.info('✌️ Server need close');
|
Logger.error('Uncaught exception:', error);
|
||||||
server.close(() => {
|
});
|
||||||
Logger.info('✌️ Server closed');
|
|
||||||
process.exit(0);
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
});
|
Logger.error('Unhandled rejection:', reason, promise);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-10
@@ -1,28 +1,25 @@
|
|||||||
import sockJs from 'sockjs';
|
import sockJs from 'sockjs';
|
||||||
import { Server } from 'http';
|
import { Server } from 'http';
|
||||||
import Logger from './logger';
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import SockService from '../services/sock';
|
import SockService from '../services/sock';
|
||||||
import config from '../config/index';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { getPlatform } from '../config/util';
|
import { getPlatform } from '../config/util';
|
||||||
|
import { shareStore } from '../shared/store';
|
||||||
|
|
||||||
export default async ({ server }: { server: Server }) => {
|
export default async ({ server }: { server: Server }) => {
|
||||||
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||||
const sockService = Container.get(SockService);
|
const sockService = Container.get(SockService);
|
||||||
|
|
||||||
echo.on('connection', (conn) => {
|
echo.on('connection', async (conn) => {
|
||||||
if (!conn.headers || !conn.url || !conn.pathname) {
|
if (!conn.headers || !conn.url || !conn.pathname) {
|
||||||
conn.close('404');
|
conn.close('404');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
const authInfo = await shareStore.getAuthInfo();
|
||||||
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||||
if (data) {
|
if (authInfo) {
|
||||||
const { token = '', tokens = {} } = JSON.parse(data);
|
const { token = '', tokens = {} } = authInfo;
|
||||||
if (headerToken === token || tokens[platform] === headerToken) {
|
if (headerToken === token || tokens[platform] === headerToken) {
|
||||||
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
|
||||||
sockService.addClient(conn);
|
sockService.addClient(conn);
|
||||||
|
|
||||||
conn.on('data', (message) => {
|
conn.on('data', (message) => {
|
||||||
@@ -34,8 +31,6 @@ export default async ({ server }: { server: Server }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
conn.write(JSON.stringify({ type: 'ping', message: 'whyour' }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import { errors } from 'celebrate';
|
||||||
|
import cors from 'cors';
|
||||||
|
import { Application, NextFunction, Request, Response } from 'express';
|
||||||
|
import { expressjwt } from 'express-jwt';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import config from '../config';
|
||||||
|
import SystemService from '../services/system';
|
||||||
|
import Logger from './logger';
|
||||||
|
|
||||||
|
export default ({ app }: { app: Application }) => {
|
||||||
|
app.set('trust proxy', 'loopback');
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
expressjwt({
|
||||||
|
secret: config.secret,
|
||||||
|
algorithms: ['HS384'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.put(
|
||||||
|
'/api/reload',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.reloadSystem();
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.put(
|
||||||
|
'/api/system',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.reloadSystem('system');
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.put(
|
||||||
|
'/api/data',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.reloadSystem('data');
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const err: any = new Error('Not Found');
|
||||||
|
err['status'] = 404;
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(errors());
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
(
|
||||||
|
err: Error & { status: number },
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) => {
|
||||||
|
if (err.name === 'UnauthorizedError') {
|
||||||
|
return res
|
||||||
|
.status(err.status)
|
||||||
|
.send({ code: 401, message: err.message })
|
||||||
|
.end();
|
||||||
|
}
|
||||||
|
return next(err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
(
|
||||||
|
err: Error & { status: number },
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) => {
|
||||||
|
res.status(err.status || 500);
|
||||||
|
res.json({
|
||||||
|
code: err.status || 500,
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package com.ql.api;
|
||||||
|
|
||||||
|
message EnvItem {
|
||||||
|
optional int32 id = 1;
|
||||||
|
optional string name = 2;
|
||||||
|
optional string value = 3;
|
||||||
|
optional string remarks = 4;
|
||||||
|
optional int32 status = 5;
|
||||||
|
optional int32 position = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetEnvsRequest { string searchValue = 1; }
|
||||||
|
|
||||||
|
message CreateEnvRequest { repeated EnvItem envs = 1; }
|
||||||
|
|
||||||
|
message UpdateEnvRequest { EnvItem env = 1; }
|
||||||
|
|
||||||
|
message DeleteEnvsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message MoveEnvRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
int32 fromIndex = 2;
|
||||||
|
int32 toIndex = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DisableEnvsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message EnableEnvsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message UpdateEnvNamesRequest {
|
||||||
|
repeated int32 ids = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetEnvByIdRequest { int32 id = 1; }
|
||||||
|
|
||||||
|
message EnvsResponse {
|
||||||
|
int32 code = 1;
|
||||||
|
repeated EnvItem data = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EnvResponse {
|
||||||
|
int32 code = 1;
|
||||||
|
EnvItem data = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
int32 code = 1;
|
||||||
|
optional string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SystemNotifyRequest {
|
||||||
|
string title = 1;
|
||||||
|
string content = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Api {
|
||||||
|
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
|
||||||
|
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
|
||||||
|
rpc UpdateEnv(UpdateEnvRequest) returns (EnvResponse) {}
|
||||||
|
rpc DeleteEnvs(DeleteEnvsRequest) returns (Response) {}
|
||||||
|
rpc MoveEnv(MoveEnvRequest) returns (EnvResponse) {}
|
||||||
|
rpc DisableEnvs(DisableEnvsRequest) returns (Response) {}
|
||||||
|
rpc EnableEnvs(EnableEnvsRequest) returns (Response) {}
|
||||||
|
rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}
|
||||||
|
rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}
|
||||||
|
rpc SystemNotify(SystemNotifyRequest) returns (Response) {}
|
||||||
|
}
|
||||||
+1451
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package com.ql.cron;
|
||||||
|
|
||||||
|
service Cron {
|
||||||
|
rpc addCron(AddCronRequest) returns (AddCronResponse);
|
||||||
|
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message ISchedule { string schedule = 1; }
|
||||||
|
|
||||||
|
message ICron {
|
||||||
|
string id = 1;
|
||||||
|
string schedule = 2;
|
||||||
|
string command = 3;
|
||||||
|
repeated ISchedule extra_schedules = 4;
|
||||||
|
string name = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddCronRequest { repeated ICron crons = 1; }
|
||||||
|
|
||||||
|
message AddCronResponse {}
|
||||||
|
|
||||||
|
message DeleteCronRequest { repeated string ids = 1; }
|
||||||
|
|
||||||
|
message DeleteCronResponse {}
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v1.181.2
|
||||||
|
// protoc v3.17.3
|
||||||
|
// source: back/protos/cron.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import {
|
||||||
|
type CallOptions,
|
||||||
|
ChannelCredentials,
|
||||||
|
Client,
|
||||||
|
type ClientOptions,
|
||||||
|
type ClientUnaryCall,
|
||||||
|
type handleUnaryCall,
|
||||||
|
makeGenericClientConstructor,
|
||||||
|
Metadata,
|
||||||
|
type ServiceError,
|
||||||
|
type UntypedServiceImplementation,
|
||||||
|
} from "@grpc/grpc-js";
|
||||||
|
import _m0 from "protobufjs/minimal";
|
||||||
|
|
||||||
|
export const protobufPackage = "com.ql.cron";
|
||||||
|
|
||||||
|
export interface ISchedule {
|
||||||
|
schedule: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICron {
|
||||||
|
id: string;
|
||||||
|
schedule: string;
|
||||||
|
command: string;
|
||||||
|
extraSchedules: ISchedule[];
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddCronRequest {
|
||||||
|
crons: ICron[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddCronResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCronRequest {
|
||||||
|
ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCronResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseISchedule(): ISchedule {
|
||||||
|
return { schedule: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ISchedule = {
|
||||||
|
encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
if (message.schedule !== "") {
|
||||||
|
writer.uint32(10).string(message.schedule);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): ISchedule {
|
||||||
|
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseISchedule();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.schedule = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): ISchedule {
|
||||||
|
return { schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "" };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: ISchedule): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.schedule !== "") {
|
||||||
|
obj.schedule = message.schedule;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
|
||||||
|
return ISchedule.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
|
||||||
|
const message = createBaseISchedule();
|
||||||
|
message.schedule = object.schedule ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseICron(): ICron {
|
||||||
|
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ICron = {
|
||||||
|
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
if (message.id !== "") {
|
||||||
|
writer.uint32(10).string(message.id);
|
||||||
|
}
|
||||||
|
if (message.schedule !== "") {
|
||||||
|
writer.uint32(18).string(message.schedule);
|
||||||
|
}
|
||||||
|
if (message.command !== "") {
|
||||||
|
writer.uint32(26).string(message.command);
|
||||||
|
}
|
||||||
|
for (const v of message.extraSchedules) {
|
||||||
|
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
|
||||||
|
}
|
||||||
|
if (message.name !== "") {
|
||||||
|
writer.uint32(42).string(message.name);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
|
||||||
|
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseICron();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.id = reader.string();
|
||||||
|
continue;
|
||||||
|
case 2:
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.schedule = reader.string();
|
||||||
|
continue;
|
||||||
|
case 3:
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.command = reader.string();
|
||||||
|
continue;
|
||||||
|
case 4:
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
case 5:
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.name = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): ICron {
|
||||||
|
return {
|
||||||
|
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||||
|
schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "",
|
||||||
|
command: isSet(object.command) ? globalThis.String(object.command) : "",
|
||||||
|
extraSchedules: globalThis.Array.isArray(object?.extraSchedules)
|
||||||
|
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
|
||||||
|
: [],
|
||||||
|
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: ICron): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.id !== "") {
|
||||||
|
obj.id = message.id;
|
||||||
|
}
|
||||||
|
if (message.schedule !== "") {
|
||||||
|
obj.schedule = message.schedule;
|
||||||
|
}
|
||||||
|
if (message.command !== "") {
|
||||||
|
obj.command = message.command;
|
||||||
|
}
|
||||||
|
if (message.extraSchedules?.length) {
|
||||||
|
obj.extraSchedules = message.extraSchedules.map((e) => ISchedule.toJSON(e));
|
||||||
|
}
|
||||||
|
if (message.name !== "") {
|
||||||
|
obj.name = message.name;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
|
||||||
|
return ICron.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
|
||||||
|
const message = createBaseICron();
|
||||||
|
message.id = object.id ?? "";
|
||||||
|
message.schedule = object.schedule ?? "";
|
||||||
|
message.command = object.command ?? "";
|
||||||
|
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
||||||
|
message.name = object.name ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseAddCronRequest(): AddCronRequest {
|
||||||
|
return { crons: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddCronRequest = {
|
||||||
|
encode(message: AddCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
for (const v of message.crons) {
|
||||||
|
ICron.encode(v!, writer.uint32(10).fork()).ldelim();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
|
||||||
|
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseAddCronRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.crons.push(ICron.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): AddCronRequest {
|
||||||
|
return { crons: globalThis.Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: AddCronRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.crons?.length) {
|
||||||
|
obj.crons = message.crons.map((e) => ICron.toJSON(e));
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
|
||||||
|
return AddCronRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
|
||||||
|
const message = createBaseAddCronRequest();
|
||||||
|
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseAddCronResponse(): AddCronResponse {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddCronResponse = {
|
||||||
|
encode(_: AddCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
|
||||||
|
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseAddCronResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(_: any): AddCronResponse {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(_: AddCronResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
|
||||||
|
return AddCronResponse.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
|
||||||
|
const message = createBaseAddCronResponse();
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseDeleteCronRequest(): DeleteCronRequest {
|
||||||
|
return { ids: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteCronRequest = {
|
||||||
|
encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
for (const v of message.ids) {
|
||||||
|
writer.uint32(10).string(v!);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
|
||||||
|
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseDeleteCronRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.ids.push(reader.string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): DeleteCronRequest {
|
||||||
|
return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.String(e)) : [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: DeleteCronRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.ids?.length) {
|
||||||
|
obj.ids = message.ids;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
|
||||||
|
return DeleteCronRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
|
||||||
|
const message = createBaseDeleteCronRequest();
|
||||||
|
message.ids = object.ids?.map((e) => e) || [];
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseDeleteCronResponse(): DeleteCronResponse {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteCronResponse = {
|
||||||
|
encode(_: DeleteCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
|
||||||
|
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseDeleteCronResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(_: any): DeleteCronResponse {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(_: DeleteCronResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
|
||||||
|
return DeleteCronResponse.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
|
||||||
|
const message = createBaseDeleteCronResponse();
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CronService = typeof CronService;
|
||||||
|
export const CronService = {
|
||||||
|
addCron: {
|
||||||
|
path: "/com.ql.cron.Cron/addCron",
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: false,
|
||||||
|
requestSerialize: (value: AddCronRequest) => Buffer.from(AddCronRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
|
||||||
|
responseSerialize: (value: AddCronResponse) => Buffer.from(AddCronResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
|
||||||
|
},
|
||||||
|
delCron: {
|
||||||
|
path: "/com.ql.cron.Cron/delCron",
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: false,
|
||||||
|
requestSerialize: (value: DeleteCronRequest) => Buffer.from(DeleteCronRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
|
||||||
|
responseSerialize: (value: DeleteCronResponse) => Buffer.from(DeleteCronResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface CronServer extends UntypedServiceImplementation {
|
||||||
|
addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;
|
||||||
|
delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CronClient extends Client {
|
||||||
|
addCron(
|
||||||
|
request: AddCronRequest,
|
||||||
|
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
addCron(
|
||||||
|
request: AddCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
addCron(
|
||||||
|
request: AddCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
delCron(
|
||||||
|
request: DeleteCronRequest,
|
||||||
|
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
delCron(
|
||||||
|
request: DeleteCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
delCron(
|
||||||
|
request: DeleteCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as {
|
||||||
|
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;
|
||||||
|
service: typeof CronService;
|
||||||
|
serviceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
export type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
export type Exact<P, I extends P> = P extends Builtin ? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package com.ql.health;
|
||||||
|
|
||||||
|
message HealthCheckRequest {
|
||||||
|
string service = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HealthCheckResponse {
|
||||||
|
enum ServingStatus {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
SERVING = 1;
|
||||||
|
NOT_SERVING = 2;
|
||||||
|
SERVICE_UNKNOWN = 3;
|
||||||
|
}
|
||||||
|
ServingStatus status = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Health {
|
||||||
|
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||||
|
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v1.181.2
|
||||||
|
// protoc v3.17.3
|
||||||
|
// source: back/protos/health.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import {
|
||||||
|
type CallOptions,
|
||||||
|
ChannelCredentials,
|
||||||
|
Client,
|
||||||
|
type ClientOptions,
|
||||||
|
ClientReadableStream,
|
||||||
|
type ClientUnaryCall,
|
||||||
|
handleServerStreamingCall,
|
||||||
|
type handleUnaryCall,
|
||||||
|
makeGenericClientConstructor,
|
||||||
|
Metadata,
|
||||||
|
type ServiceError,
|
||||||
|
type UntypedServiceImplementation,
|
||||||
|
} from "@grpc/grpc-js";
|
||||||
|
import _m0 from "protobufjs/minimal";
|
||||||
|
|
||||||
|
export const protobufPackage = "com.ql.health";
|
||||||
|
|
||||||
|
export interface HealthCheckRequest {
|
||||||
|
service: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthCheckResponse {
|
||||||
|
status: HealthCheckResponse_ServingStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HealthCheckResponse_ServingStatus {
|
||||||
|
UNKNOWN = 0,
|
||||||
|
SERVING = 1,
|
||||||
|
NOT_SERVING = 2,
|
||||||
|
SERVICE_UNKNOWN = 3,
|
||||||
|
UNRECOGNIZED = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function healthCheckResponse_ServingStatusFromJSON(object: any): HealthCheckResponse_ServingStatus {
|
||||||
|
switch (object) {
|
||||||
|
case 0:
|
||||||
|
case "UNKNOWN":
|
||||||
|
return HealthCheckResponse_ServingStatus.UNKNOWN;
|
||||||
|
case 1:
|
||||||
|
case "SERVING":
|
||||||
|
return HealthCheckResponse_ServingStatus.SERVING;
|
||||||
|
case 2:
|
||||||
|
case "NOT_SERVING":
|
||||||
|
return HealthCheckResponse_ServingStatus.NOT_SERVING;
|
||||||
|
case 3:
|
||||||
|
case "SERVICE_UNKNOWN":
|
||||||
|
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
|
||||||
|
case -1:
|
||||||
|
case "UNRECOGNIZED":
|
||||||
|
default:
|
||||||
|
return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function healthCheckResponse_ServingStatusToJSON(object: HealthCheckResponse_ServingStatus): string {
|
||||||
|
switch (object) {
|
||||||
|
case HealthCheckResponse_ServingStatus.UNKNOWN:
|
||||||
|
return "UNKNOWN";
|
||||||
|
case HealthCheckResponse_ServingStatus.SERVING:
|
||||||
|
return "SERVING";
|
||||||
|
case HealthCheckResponse_ServingStatus.NOT_SERVING:
|
||||||
|
return "NOT_SERVING";
|
||||||
|
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
|
||||||
|
return "SERVICE_UNKNOWN";
|
||||||
|
case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
|
||||||
|
default:
|
||||||
|
return "UNRECOGNIZED";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseHealthCheckRequest(): HealthCheckRequest {
|
||||||
|
return { service: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthCheckRequest = {
|
||||||
|
encode(message: HealthCheckRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
if (message.service !== "") {
|
||||||
|
writer.uint32(10).string(message.service);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
|
||||||
|
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseHealthCheckRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.service = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): HealthCheckRequest {
|
||||||
|
return { service: isSet(object.service) ? globalThis.String(object.service) : "" };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: HealthCheckRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.service !== "") {
|
||||||
|
obj.service = message.service;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
|
||||||
|
return HealthCheckRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
|
||||||
|
const message = createBaseHealthCheckRequest();
|
||||||
|
message.service = object.service ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseHealthCheckResponse(): HealthCheckResponse {
|
||||||
|
return { status: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthCheckResponse = {
|
||||||
|
encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
if (message.status !== 0) {
|
||||||
|
writer.uint32(8).int32(message.status);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
|
||||||
|
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseHealthCheckResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.status = reader.int32() as any;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): HealthCheckResponse {
|
||||||
|
return { status: isSet(object.status) ? healthCheckResponse_ServingStatusFromJSON(object.status) : 0 };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: HealthCheckResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.status !== 0) {
|
||||||
|
obj.status = healthCheckResponse_ServingStatusToJSON(message.status);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
|
||||||
|
return HealthCheckResponse.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
|
||||||
|
const message = createBaseHealthCheckResponse();
|
||||||
|
message.status = object.status ?? 0;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HealthService = typeof HealthService;
|
||||||
|
export const HealthService = {
|
||||||
|
check: {
|
||||||
|
path: "/com.ql.health.Health/Check",
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: false,
|
||||||
|
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
||||||
|
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
path: "/com.ql.health.Health/Watch",
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: true,
|
||||||
|
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
||||||
|
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface HealthServer extends UntypedServiceImplementation {
|
||||||
|
check: handleUnaryCall<HealthCheckRequest, HealthCheckResponse>;
|
||||||
|
watch: handleServerStreamingCall<HealthCheckRequest, HealthCheckResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthClient extends Client {
|
||||||
|
check(
|
||||||
|
request: HealthCheckRequest,
|
||||||
|
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
check(
|
||||||
|
request: HealthCheckRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
check(
|
||||||
|
request: HealthCheckRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
watch(request: HealthCheckRequest, options?: Partial<CallOptions>): ClientReadableStream<HealthCheckResponse>;
|
||||||
|
watch(
|
||||||
|
request: HealthCheckRequest,
|
||||||
|
metadata?: Metadata,
|
||||||
|
options?: Partial<CallOptions>,
|
||||||
|
): ClientReadableStream<HealthCheckResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as {
|
||||||
|
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;
|
||||||
|
service: typeof HealthService;
|
||||||
|
serviceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
export type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
export type Exact<P, I extends P> = P extends Builtin ? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
+18
-13
@@ -1,30 +1,35 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { exec } from 'child_process';
|
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
import { HealthClient } from './protos/health';
|
||||||
|
import { credentials } from '@grpc/grpc-js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const client = new HealthClient(
|
||||||
|
`0.0.0.0:${config.cronPort}`,
|
||||||
|
credentials.createInsecure(),
|
||||||
|
{ 'grpc.enable_http_proxy': 0 },
|
||||||
|
);
|
||||||
|
|
||||||
app.get('/api/public/panel/log', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
exec(
|
client.check({ service: 'cron' }, (err, response) => {
|
||||||
'pm2 logs panel --lines 500 --nostream --timestamp',
|
if (err) {
|
||||||
(err, stdout, stderr) => {
|
return res.status(200).send({ code: 500, error: err });
|
||||||
if (err || stderr) {
|
}
|
||||||
return res.send({ code: 400, message: (err && err.message) || stderr });
|
return res.status(200).send({ code: 200, data: response });
|
||||||
}
|
});
|
||||||
return res.send({ code: 200, data: stdout });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app
|
app
|
||||||
.listen(config.publicPort, async () => {
|
.listen(config.publicPort, '0.0.0.0', async () => {
|
||||||
await require('./loaders/sentry').default({ expressApp: app });
|
|
||||||
await require('./loaders/db').default();
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
Logger.debug(`✌️ 公共服务启动成功!`);
|
Logger.debug(`✌️ 公共服务启动成功!`);
|
||||||
|
console.debug(`✌️ 公共服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import schedule from 'node-schedule';
|
|
||||||
import express from 'express';
|
|
||||||
import { exec } from 'child_process';
|
|
||||||
import Logger from './loaders/logger';
|
|
||||||
import { CrontabModel, CrontabStatus } from './data/cron';
|
|
||||||
import config from './config';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
const run = async () => {
|
|
||||||
CrontabModel.findAll({ where: {} })
|
|
||||||
.then((docs) => {
|
|
||||||
if (docs && docs.length > 0) {
|
|
||||||
for (let i = 0; i < docs.length; i++) {
|
|
||||||
const task = docs[i];
|
|
||||||
const _schedule = task.schedule && task.schedule.split(/ +/);
|
|
||||||
if (
|
|
||||||
_schedule &&
|
|
||||||
_schedule.length > 5 &&
|
|
||||||
task.status !== CrontabStatus.disabled &&
|
|
||||||
!task.isDisabled &&
|
|
||||||
task.schedule
|
|
||||||
) {
|
|
||||||
schedule.scheduleJob(task.schedule, function () {
|
|
||||||
let command = task.command as string;
|
|
||||||
if (!command.includes('task ') && !command.includes('ql ')) {
|
|
||||||
command = `task ${command}`;
|
|
||||||
}
|
|
||||||
exec(`ID=${task.id} ${command}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
Logger.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
app
|
|
||||||
.listen(config.cronPort, async () => {
|
|
||||||
await require('./loaders/sentry').default({ expressApp: app });
|
|
||||||
await require('./loaders/db').default();
|
|
||||||
|
|
||||||
await run();
|
|
||||||
Logger.debug('定时任务服务启动成功!');
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
Logger.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { AddCronRequest, AddCronResponse } from '../protos/cron';
|
||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { scheduleStacks } from './data';
|
||||||
|
import { runCron } from '../shared/runCron';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
const addCron = (
|
||||||
|
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
|
||||||
|
callback: sendUnaryData<AddCronResponse>,
|
||||||
|
) => {
|
||||||
|
for (const item of call.request.crons) {
|
||||||
|
const { id, schedule, command, extraSchedules, name } = item;
|
||||||
|
if (scheduleStacks.has(id)) {
|
||||||
|
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
schedule,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (extraSchedules?.length) {
|
||||||
|
extraSchedules.forEach((x) => {
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
x.schedule,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleStacks.set(id, [
|
||||||
|
nodeSchedule.scheduleJob(id, schedule, async () => {
|
||||||
|
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||||
|
runCron(command, item);
|
||||||
|
}),
|
||||||
|
...(extraSchedules?.length
|
||||||
|
? extraSchedules.map((x) =>
|
||||||
|
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
||||||
|
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||||
|
runCron(command, item);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: []),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { addCron };
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import EnvService from '../services/env';
|
||||||
|
import { sendUnaryData, ServerUnaryCall } from '@grpc/grpc-js';
|
||||||
|
import {
|
||||||
|
CreateEnvRequest,
|
||||||
|
DeleteEnvsRequest,
|
||||||
|
DisableEnvsRequest,
|
||||||
|
EnableEnvsRequest,
|
||||||
|
EnvItem,
|
||||||
|
EnvResponse,
|
||||||
|
EnvsResponse,
|
||||||
|
GetEnvByIdRequest,
|
||||||
|
GetEnvsRequest,
|
||||||
|
MoveEnvRequest,
|
||||||
|
Response,
|
||||||
|
SystemNotifyRequest,
|
||||||
|
UpdateEnvNamesRequest,
|
||||||
|
UpdateEnvRequest,
|
||||||
|
} from '../protos/api';
|
||||||
|
import LoggerInstance from '../loaders/logger';
|
||||||
|
import pick from 'lodash/pick';
|
||||||
|
import SystemService from '../services/system';
|
||||||
|
|
||||||
|
Container.set('logger', LoggerInstance);
|
||||||
|
|
||||||
|
export const getEnvs = async (
|
||||||
|
call: ServerUnaryCall<GetEnvsRequest, EnvsResponse>,
|
||||||
|
callback: sendUnaryData<EnvsResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.envs(call.request.searchValue);
|
||||||
|
callback(null, {
|
||||||
|
code: 200,
|
||||||
|
data: data.map((x) => ({ ...x, remarks: x.remarks || '' })),
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(null, {
|
||||||
|
code: 500,
|
||||||
|
data: [],
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createEnv = async (
|
||||||
|
call: ServerUnaryCall<CreateEnvRequest, EnvsResponse>,
|
||||||
|
callback: sendUnaryData<EnvsResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.create(call.request.envs);
|
||||||
|
callback(null, { code: 200, data });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateEnv = async (
|
||||||
|
call: ServerUnaryCall<UpdateEnvRequest, EnvResponse>,
|
||||||
|
callback: sendUnaryData<EnvResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.update(
|
||||||
|
pick(call.request.env, ['id', 'name', 'value', 'remark']) as EnvItem,
|
||||||
|
);
|
||||||
|
callback(null, { code: 200, data });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteEnvs = async (
|
||||||
|
call: ServerUnaryCall<DeleteEnvsRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
await envService.remove(call.request.ids);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moveEnv = async (
|
||||||
|
call: ServerUnaryCall<MoveEnvRequest, EnvResponse>,
|
||||||
|
callback: sendUnaryData<EnvResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.move(call.request.id, {
|
||||||
|
fromIndex: call.request.fromIndex,
|
||||||
|
toIndex: call.request.toIndex,
|
||||||
|
});
|
||||||
|
callback(null, { code: 200, data });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const disableEnvs = async (
|
||||||
|
call: ServerUnaryCall<DisableEnvsRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
await envService.disabled(call.request.ids);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const enableEnvs = async (
|
||||||
|
call: ServerUnaryCall<EnableEnvsRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
await envService.enabled(call.request.ids);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateEnvNames = async (
|
||||||
|
call: ServerUnaryCall<UpdateEnvNamesRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
await envService.updateNames({
|
||||||
|
ids: call.request.ids,
|
||||||
|
name: call.request.name,
|
||||||
|
});
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEnvById = async (
|
||||||
|
call: ServerUnaryCall<GetEnvByIdRequest, EnvResponse>,
|
||||||
|
callback: sendUnaryData<EnvResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.getDb({ id: call.request.id });
|
||||||
|
callback(null, {
|
||||||
|
code: 200,
|
||||||
|
data: { ...data, remarks: data.remarks || '' },
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const systemNotify = async (
|
||||||
|
call: ServerUnaryCall<SystemNotifyRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const data = await systemService.notify(call.request);
|
||||||
|
callback(null, data);
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { credentials } from '@grpc/grpc-js';
|
||||||
|
import {
|
||||||
|
AddCronRequest,
|
||||||
|
AddCronResponse,
|
||||||
|
CronClient,
|
||||||
|
DeleteCronRequest,
|
||||||
|
DeleteCronResponse,
|
||||||
|
} from '../protos/cron';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
private client = new CronClient(
|
||||||
|
`0.0.0.0:${config.cronPort}`,
|
||||||
|
credentials.createInsecure(),
|
||||||
|
{ 'grpc.enable_http_proxy': 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.addCron({ crons: request }, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.delCron({ ids: request }, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Client();
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { ToadScheduler } from 'toad-scheduler';
|
||||||
|
|
||||||
|
export const scheduleStacks = new Map<string, nodeSchedule.Job[]>();
|
||||||
|
|
||||||
|
export const intervalSchedule = new ToadScheduler();
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
|
||||||
|
import { scheduleStacks } from './data';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
const delCron = (
|
||||||
|
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
|
||||||
|
callback: sendUnaryData<DeleteCronResponse>,
|
||||||
|
) => {
|
||||||
|
for (const id of call.request.ids) {
|
||||||
|
if (scheduleStacks.has(id)) {
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][取消定时任务], 任务ID: %s',
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
scheduleStacks.get(id)?.forEach(x => x.cancel());
|
||||||
|
scheduleStacks.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { delCron };
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
|
||||||
|
import config from '../config';
|
||||||
|
import { promiseExec } from '../config/util';
|
||||||
|
|
||||||
|
const check = async (
|
||||||
|
call: ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,
|
||||||
|
callback: sendUnaryData<HealthCheckResponse>,
|
||||||
|
) => {
|
||||||
|
switch (call.request.service) {
|
||||||
|
case 'cron':
|
||||||
|
const res = await promiseExec(
|
||||||
|
`curl -s --noproxy '*' http://0.0.0.0:${config.port}/api/system`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.includes('200')) {
|
||||||
|
return callback(null, { status: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelErrLog = await promiseExec(
|
||||||
|
`tail -n 300 ~/.pm2/logs/panel-error.log`,
|
||||||
|
);
|
||||||
|
const scheduleErrLog = await promiseExec(
|
||||||
|
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
|
||||||
|
);
|
||||||
|
return callback(
|
||||||
|
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`.trim()),
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return callback(null, { status: 1 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { check };
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Server, ServerCredentials } from '@grpc/grpc-js';
|
||||||
|
import { CronService } from '../protos/cron';
|
||||||
|
import { addCron } from './addCron';
|
||||||
|
import { delCron } from './delCron';
|
||||||
|
import { HealthService } from '../protos/health';
|
||||||
|
import { check } from './health';
|
||||||
|
import config from '../config';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import { ApiService } from '../protos/api';
|
||||||
|
import * as Api from './api';
|
||||||
|
|
||||||
|
const server = new Server({ 'grpc.enable_http_proxy': 0 });
|
||||||
|
server.addService(HealthService, { check });
|
||||||
|
server.addService(CronService, { addCron, delCron });
|
||||||
|
server.addService(ApiService, Api);
|
||||||
|
server.bindAsync(
|
||||||
|
`0.0.0.0:${config.cronPort}`,
|
||||||
|
ServerCredentials.createInsecure(),
|
||||||
|
(err, port) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
Logger.debug(`✌️ 定时服务启动成功!`);
|
||||||
|
console.debug(`✌️ 定时服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import path, { join } from 'path';
|
||||||
|
import config from '../config';
|
||||||
|
import { getFileContentByName } from '../config/util';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class ConfigService {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
public async getFile(filePath: string, res: Response) {
|
||||||
|
let content = '';
|
||||||
|
const avaliablePath = [config.rootPath, config.configPath].map((x) =>
|
||||||
|
path.resolve(x, filePath),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.blackFileList.includes(filePath) ||
|
||||||
|
avaliablePath.every(
|
||||||
|
(x) =>
|
||||||
|
!x.startsWith(config.scriptPath) && !x.startsWith(config.configPath),
|
||||||
|
) ||
|
||||||
|
!filePath
|
||||||
|
) {
|
||||||
|
return res.send({ code: 403, message: '文件无法访问' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath.startsWith('sample/')) {
|
||||||
|
const res = await got.get(
|
||||||
|
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
|
||||||
|
);
|
||||||
|
content = res.body;
|
||||||
|
} else if (filePath.startsWith('data/scripts/')) {
|
||||||
|
content = await getFileContentByName(join(config.rootPath, filePath));
|
||||||
|
} else {
|
||||||
|
content = await getFileContentByName(join(config.configPath, filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ code: 200, data: content });
|
||||||
|
}
|
||||||
|
}
|
||||||
+268
-238
@@ -2,23 +2,34 @@ import { Service, Inject } from 'typedi';
|
|||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
import { exec, execSync, spawn } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs/promises';
|
||||||
import cron_parser from 'cron-parser';
|
import cron_parser from 'cron-parser';
|
||||||
import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
|
import {
|
||||||
import { promises, existsSync } from 'fs';
|
getFileContentByName,
|
||||||
import { promisify } from 'util';
|
fileExist,
|
||||||
import { Op } from 'sequelize';
|
killTask,
|
||||||
|
getUniqPath,
|
||||||
|
safeJSONParse,
|
||||||
|
} from '../config/util';
|
||||||
|
import { Op, where, col as colFn, FindOptions, fn, Order } from 'sequelize';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
||||||
|
import cronClient from '../schedule/client';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
import { spawn } from 'cross-spawn';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import pickBy from 'lodash/pickBy';
|
||||||
|
import omit from 'lodash/omit';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
private isSixCron(cron: Crontab) {
|
private isNodeCron(cron: Crontab) {
|
||||||
const { schedule } = cron;
|
const { schedule, extra_schedules } = cron;
|
||||||
if (schedule?.split(/ +/).length === 6) {
|
if (Number(schedule?.split(/ +/).length) > 5 || extra_schedules?.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -28,7 +39,18 @@ export default class CronService {
|
|||||||
const tab = new Crontab(payload);
|
const tab = new Crontab(payload);
|
||||||
tab.saved = false;
|
tab.saved = false;
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
await this.set_crontab(this.isSixCron(doc));
|
if (this.isNodeCron(doc)) {
|
||||||
|
await cronClient.addCron([
|
||||||
|
{
|
||||||
|
name: doc.name || '',
|
||||||
|
id: String(doc.id),
|
||||||
|
schedule: doc.schedule!,
|
||||||
|
command: this.makeCommand(doc),
|
||||||
|
extraSchedules: doc.extra_schedules || [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await this.set_crontab();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,9 +59,28 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Crontab): Promise<Crontab> {
|
public async update(payload: Crontab): Promise<Crontab> {
|
||||||
payload.saved = false;
|
const doc = await this.getDb({ id: payload.id });
|
||||||
const newDoc = await this.updateDb(payload);
|
const tab = new Crontab({ ...doc, ...payload });
|
||||||
await this.set_crontab(this.isSixCron(newDoc));
|
tab.saved = false;
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
if (doc.isDisabled === 1) {
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
if (this.isNodeCron(doc)) {
|
||||||
|
await cronClient.delCron([String(doc.id)]);
|
||||||
|
}
|
||||||
|
if (this.isNodeCron(newDoc)) {
|
||||||
|
await cronClient.addCron([
|
||||||
|
{
|
||||||
|
name: doc.name || '',
|
||||||
|
id: String(newDoc.id),
|
||||||
|
schedule: newDoc.schedule!,
|
||||||
|
command: this.makeCommand(newDoc),
|
||||||
|
extraSchedules: newDoc.extra_schedules || [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await this.set_crontab();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +104,7 @@ export default class CronService {
|
|||||||
last_running_time: number;
|
last_running_time: number;
|
||||||
last_execution_time: number;
|
last_execution_time: number;
|
||||||
}) {
|
}) {
|
||||||
const options: any = {
|
let options: any = {
|
||||||
status,
|
status,
|
||||||
pid,
|
pid,
|
||||||
log_path,
|
log_path,
|
||||||
@@ -73,12 +114,28 @@ export default class CronService {
|
|||||||
options.last_running_time = last_running_time;
|
options.last_running_time = last_running_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await CrontabModel.update({ ...options }, { where: { id: ids } });
|
for (const id of ids) {
|
||||||
|
let cron;
|
||||||
|
try {
|
||||||
|
cron = await this.getDb({ id });
|
||||||
|
} catch (err) {}
|
||||||
|
if (!cron) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
|
||||||
|
options = omit(options, ['status', 'log_path', 'pid']);
|
||||||
|
}
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ ...pickBy(options, (v) => v === 0 || !!v) },
|
||||||
|
{ where: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
await CrontabModel.destroy({ where: { id: ids } });
|
await CrontabModel.destroy({ where: { id: ids } });
|
||||||
await this.set_crontab(true);
|
await cronClient.delCron(ids.map(String));
|
||||||
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pin(ids: number[]) {
|
public async pin(ids: number[]) {
|
||||||
@@ -115,8 +172,9 @@ export default class CronService {
|
|||||||
|
|
||||||
private formatViewQuery(query: any, viewQuery: any) {
|
private formatViewQuery(query: any, viewQuery: any) {
|
||||||
if (viewQuery.filters && viewQuery.filters.length > 0) {
|
if (viewQuery.filters && viewQuery.filters.length > 0) {
|
||||||
if (!query[Op.and]) {
|
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
|
||||||
query[Op.and] = [];
|
if (!query[primaryOperate]) {
|
||||||
|
query[primaryOperate] = [];
|
||||||
}
|
}
|
||||||
for (const col of viewQuery.filters) {
|
for (const col of viewQuery.filters) {
|
||||||
const { property, value, operation } = col;
|
const { property, value, operation } = col;
|
||||||
@@ -135,7 +193,7 @@ export default class CronService {
|
|||||||
case 'In':
|
case 'In':
|
||||||
q[Op.or] = [
|
q[Op.or] = [
|
||||||
{
|
{
|
||||||
[property]: value,
|
[property]: Array.isArray(value) ? value : [value],
|
||||||
},
|
},
|
||||||
property === 'status' && value.includes(2)
|
property === 'status' && value.includes(2)
|
||||||
? { isDisabled: 1 }
|
? { isDisabled: 1 }
|
||||||
@@ -146,7 +204,7 @@ export default class CronService {
|
|||||||
q[Op.and] = [
|
q[Op.and] = [
|
||||||
{
|
{
|
||||||
[property]: {
|
[property]: {
|
||||||
[Op.notIn]: value,
|
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
property === 'status' && value.includes(2)
|
property === 'status' && value.includes(2)
|
||||||
@@ -159,13 +217,23 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
if (operate && operate2) {
|
if (operate && operate2) {
|
||||||
q[property] = {
|
q[property] = {
|
||||||
[operate2]: [
|
[Op.or]: [
|
||||||
{ [operate]: `%${value}%` },
|
{
|
||||||
{ [operate]: `%${encodeURIComponent(value)}%` },
|
[operate2]: [
|
||||||
|
{ [operate]: `%${value}%` },
|
||||||
|
{ [operate]: `%${encodeURI(value)}%` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[operate2]: [
|
||||||
|
where(colFn(property), operate, `%${value}%`),
|
||||||
|
where(colFn(property), operate, `%${encodeURI(value)}%`),
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
query[Op.and].push(q);
|
query[primaryOperate].push(q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +254,7 @@ export default class CronService {
|
|||||||
q[column] = {
|
q[column] = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${textArray[1]}%` },
|
{ [Op.like]: `%${textArray[1]}%` },
|
||||||
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
{ [Op.like]: `%${encodeURI(textArray[1])}%` },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@@ -194,7 +262,7 @@ export default class CronService {
|
|||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchText}%` },
|
{ [Op.like]: `%${searchText}%` },
|
||||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
{ [Op.like]: `%${encodeURI(searchText)}%` },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
q[Op.or] = [
|
q[Op.or] = [
|
||||||
@@ -225,8 +293,15 @@ export default class CronService {
|
|||||||
const filterKeys: any = Object.keys(filterQuery);
|
const filterKeys: any = Object.keys(filterQuery);
|
||||||
for (const key of filterKeys) {
|
for (const key of filterKeys) {
|
||||||
let q: any = {};
|
let q: any = {};
|
||||||
if (filterKeys[key]) {
|
if (!filterQuery[key]) continue;
|
||||||
q[key] = filterKeys[key];
|
if (key === 'status') {
|
||||||
|
if (filterQuery[key].includes(CrontabStatus.disabled)) {
|
||||||
|
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
|
||||||
|
} else {
|
||||||
|
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
q[key] = filterQuery[key];
|
||||||
}
|
}
|
||||||
query[Op.and].push(q);
|
query[Op.and].push(q);
|
||||||
}
|
}
|
||||||
@@ -241,6 +316,19 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async find({
|
||||||
|
log_path,
|
||||||
|
}: {
|
||||||
|
log_path: string;
|
||||||
|
}): Promise<Crontab | null> {
|
||||||
|
try {
|
||||||
|
const result = await CrontabModel.findOne({ where: { log_path } });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async crontabs(params?: {
|
public async crontabs(params?: {
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
page: string;
|
page: string;
|
||||||
@@ -252,9 +340,9 @@ export default class CronService {
|
|||||||
const searchText = params?.searchValue;
|
const searchText = params?.searchValue;
|
||||||
const page = Number(params?.page || '0');
|
const page = Number(params?.page || '0');
|
||||||
const size = Number(params?.size || '0');
|
const size = Number(params?.size || '0');
|
||||||
const viewQuery = JSON.parse(params?.queryString || '{}');
|
const viewQuery = safeJSONParse(params?.queryString);
|
||||||
const filterQuery = JSON.parse(params?.filters || '{}');
|
const filterQuery = safeJSONParse(params?.filters);
|
||||||
const sorterQuery = JSON.parse(params?.sorter || '{}');
|
const sorterQuery = safeJSONParse(params?.sorter);
|
||||||
|
|
||||||
let query: any = {};
|
let query: any = {};
|
||||||
let order = [
|
let order = [
|
||||||
@@ -275,9 +363,9 @@ export default class CronService {
|
|||||||
order.unshift([field, type]);
|
order.unshift([field, type]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let condition: any = {
|
let condition: FindOptions<Crontab> = {
|
||||||
where: query,
|
where: query,
|
||||||
order: order,
|
order: order as Order,
|
||||||
};
|
};
|
||||||
if (page && size) {
|
if (page && size) {
|
||||||
condition.offset = (page - 1) * size;
|
condition.offset = (page - 1) * size;
|
||||||
@@ -292,9 +380,12 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<Crontab> {
|
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {
|
||||||
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as Crontab);
|
if (!doc) {
|
||||||
|
throw new Error(`Cron ${JSON.stringify(query)} not found`);
|
||||||
|
}
|
||||||
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run(ids: number[]) {
|
public async run(ids: number[]) {
|
||||||
@@ -302,10 +393,9 @@ export default class CronService {
|
|||||||
{ status: CrontabStatus.queued },
|
{ status: CrontabStatus.queued },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
concurrentRun(
|
ids.forEach((id) => {
|
||||||
ids.map((id) => async () => await this.runSingle(id)),
|
this.runSingle(id);
|
||||||
10,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(ids: number[]) {
|
public async stop(ids: number[]) {
|
||||||
@@ -313,31 +403,11 @@ export default class CronService {
|
|||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
if (doc.pid) {
|
if (doc.pid) {
|
||||||
try {
|
try {
|
||||||
process.kill(-doc.pid);
|
await killTask(doc.pid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.silly(error);
|
this.logger.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const err = await this.killTask(doc.command);
|
|
||||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
|
||||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
|
||||||
|
|
||||||
const endTime = dayjs();
|
|
||||||
const diffTimeStr = doc.last_execution_time
|
|
||||||
? `,耗时 ${endTime.diff(
|
|
||||||
dayjs(doc.last_execution_time * 1000),
|
|
||||||
'second',
|
|
||||||
)}`
|
|
||||||
: '';
|
|
||||||
if (logFileExist) {
|
|
||||||
const str = err ? `\n${err}` : '';
|
|
||||||
fs.appendFileSync(
|
|
||||||
`${absolutePath}`,
|
|
||||||
`${str}\n## 执行结束... ${endTime.format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
)}${diffTimeStr}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
@@ -346,112 +416,102 @@ export default class CronService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async killTask(name: string) {
|
private async runSingle(cronId: number): Promise<number | void> {
|
||||||
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
return taskLimit.manualRunWithCronLimit(() => {
|
||||||
const execAsync = promisify(exec);
|
return new Promise(async (resolve: any) => {
|
||||||
try {
|
const cron = await this.getDb({ id: cronId });
|
||||||
let pid = (await execAsync(taskCommand)).stdout;
|
const params = {
|
||||||
if (pid) {
|
name: cron.name,
|
||||||
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
|
command: cron.command,
|
||||||
} else {
|
schedule: cron.schedule,
|
||||||
return;
|
extraSchedules: cron.extra_schedules,
|
||||||
}
|
};
|
||||||
let pids = pid.match(/\(\d+/g);
|
if (cron.status !== CrontabStatus.queued) {
|
||||||
const killLogs = [];
|
resolve(params);
|
||||||
if (pids && pids.length > 0) {
|
return;
|
||||||
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
|
|
||||||
pids = pids.slice(0, 3);
|
|
||||||
for (const id of pids) {
|
|
||||||
const c = `kill -9 ${id.slice(1)}`;
|
|
||||||
try {
|
|
||||||
const { stdout, stderr } = await execAsync(c);
|
|
||||||
if (stderr) {
|
|
||||||
killLogs.push(stderr);
|
|
||||||
}
|
|
||||||
if (stdout) {
|
|
||||||
killLogs.push(stdout);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
killLogs.push(error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
|
||||||
} catch (e) {
|
|
||||||
return JSON.stringify(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runSingle(cronId: number): Promise<number> {
|
|
||||||
return new Promise(async (resolve: any) => {
|
|
||||||
const cron = await this.getDb({ id: cronId });
|
|
||||||
if (cron.status !== CrontabStatus.queued) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { id, command, log_path } = cron;
|
|
||||||
const absolutePath = path.resolve(config.logPath, `${log_path}`);
|
|
||||||
const logFileExist = log_path && (await fileExist(absolutePath));
|
|
||||||
|
|
||||||
this.logger.silly('Running job');
|
|
||||||
this.logger.silly('ID: ' + id);
|
|
||||||
this.logger.silly('Original command: ' + command);
|
|
||||||
|
|
||||||
let cmdStr = command;
|
|
||||||
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
|
||||||
cmdStr = `task ${cmdStr}`;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
cmdStr.endsWith('.js') ||
|
|
||||||
cmdStr.endsWith('.py') ||
|
|
||||||
cmdStr.endsWith('.pyc') ||
|
|
||||||
cmdStr.endsWith('.sh') ||
|
|
||||||
cmdStr.endsWith('.ts')
|
|
||||||
) {
|
|
||||||
cmdStr = `${cmdStr} now`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
|
|
||||||
|
|
||||||
await CrontabModel.update(
|
|
||||||
{ status: CrontabStatus.running, pid: cp.pid },
|
|
||||||
{ where: { id } },
|
|
||||||
);
|
|
||||||
cp.stderr.on('data', (data) => {
|
|
||||||
if (logFileExist) {
|
|
||||||
fs.appendFileSync(`${absolutePath}`, `${data}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cp.on('error', (err) => {
|
|
||||||
if (logFileExist) {
|
|
||||||
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('exit', async (code, signal) => {
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
cp.on('close', async (code) => {
|
let { id, command, log_path } = cron;
|
||||||
|
const uniqPath = await getUniqPath(command, `${id}`);
|
||||||
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
|
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||||
|
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
|
||||||
|
await fs.mkdir(logDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
const logPath = `${uniqPath}/${logTime}.log`;
|
||||||
|
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
||||||
|
const cp = spawn(
|
||||||
|
`real_log_path=${logPath} no_delay=true ${this.makeCommand(
|
||||||
|
cron,
|
||||||
|
true,
|
||||||
|
)}`,
|
||||||
|
{ shell: '/bin/bash' },
|
||||||
|
);
|
||||||
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.idle, pid: undefined },
|
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
|
||||||
{ where: { id } },
|
{ where: { id } },
|
||||||
);
|
);
|
||||||
resolve();
|
cp.stdout.on('data', async (data) => {
|
||||||
|
await fs.appendFile(absolutePath, data.toString());
|
||||||
|
});
|
||||||
|
cp.stderr.on('data', async (data) => {
|
||||||
|
this.logger.info(
|
||||||
|
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
command,
|
||||||
|
data.toString(),
|
||||||
|
);
|
||||||
|
await fs.appendFile(absolutePath, data.toString());
|
||||||
|
});
|
||||||
|
cp.on('error', async (err) => {
|
||||||
|
this.logger.error(
|
||||||
|
'[panel][创建任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
command,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
await fs.appendFile(absolutePath, JSON.stringify(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code) => {
|
||||||
|
this.logger.info(
|
||||||
|
'[panel][执行任务结束] 参数: %s, 退出码: %j',
|
||||||
|
JSON.stringify(params),
|
||||||
|
code,
|
||||||
|
);
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
|
{ where: { id } },
|
||||||
|
);
|
||||||
|
resolve({ ...params, pid: cp.pid, code });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
public async disabled(ids: number[]) {
|
||||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||||
await this.set_crontab(true);
|
await cronClient.delCron(ids.map(String));
|
||||||
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
public async enabled(ids: number[]) {
|
||||||
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||||
await this.set_crontab(true);
|
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||||
|
const sixCron = docs
|
||||||
|
.filter((x) => this.isNodeCron(x))
|
||||||
|
.map((doc) => ({
|
||||||
|
name: doc.name || '',
|
||||||
|
id: String(doc.id),
|
||||||
|
schedule: doc.schedule!,
|
||||||
|
command: this.makeCommand(doc),
|
||||||
|
extraSchedules: doc.extra_schedules || [],
|
||||||
|
}));
|
||||||
|
await cronClient.addCron(sixCron);
|
||||||
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(id: number) {
|
public async log(id: number) {
|
||||||
@@ -463,130 +523,88 @@ export default class CronService {
|
|||||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||||
if (logFileExist) {
|
if (logFileExist) {
|
||||||
return getFileContentByName(`${absolutePath}`);
|
return await getFileContentByName(`${absolutePath}`);
|
||||||
}
|
|
||||||
const [, commandStr, url] = doc.command.split(/ +/);
|
|
||||||
let logPath = this.getKey(commandStr);
|
|
||||||
const isQlCommand = doc.command.startsWith('ql ');
|
|
||||||
const key =
|
|
||||||
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
|
|
||||||
logPath;
|
|
||||||
if (isQlCommand) {
|
|
||||||
logPath = 'update';
|
|
||||||
}
|
|
||||||
let logDir = `${config.logPath}${logPath}`;
|
|
||||||
if (existsSync(logDir)) {
|
|
||||||
let files = await promises.readdir(logDir);
|
|
||||||
if (isQlCommand) {
|
|
||||||
files = files.filter((x) => x.includes(key));
|
|
||||||
}
|
|
||||||
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
|
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return '任务未运行';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logs(id: number) {
|
public async logs(id: number) {
|
||||||
const doc = await this.getDb({ id });
|
const doc = await this.getDb({ id });
|
||||||
if (!doc) {
|
if (!doc || !doc.log_path) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.log_path) {
|
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
const dir = path.resolve(config.logPath, relativeDir);
|
||||||
const dir = path.resolve(config.logPath, relativeDir);
|
const dirExist = await fileExist(dir);
|
||||||
if (existsSync(dir)) {
|
if (dirExist) {
|
||||||
let files = await promises.readdir(dir);
|
let files = await fs.readdir(dir);
|
||||||
return files
|
return (
|
||||||
.map((x) => ({
|
await Promise.all(
|
||||||
|
files.map(async (x) => ({
|
||||||
filename: x,
|
filename: x,
|
||||||
directory: relativeDir.replace(config.logPath, ''),
|
directory: relativeDir.replace(config.logPath, ''),
|
||||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(),
|
||||||
}))
|
})),
|
||||||
.sort((a, b) => b.time - a.time);
|
)
|
||||||
}
|
).sort((a, b) => b.time - a.time);
|
||||||
}
|
|
||||||
|
|
||||||
const [, commandStr, url] = doc.command.split(/ +/);
|
|
||||||
let logPath = this.getKey(commandStr);
|
|
||||||
const isQlCommand = doc.command.startsWith('ql ');
|
|
||||||
const key =
|
|
||||||
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
|
|
||||||
logPath;
|
|
||||||
if (isQlCommand) {
|
|
||||||
logPath = 'update';
|
|
||||||
}
|
|
||||||
let logDir = `${config.logPath}${logPath}`;
|
|
||||||
if (existsSync(logDir)) {
|
|
||||||
let files = await promises.readdir(logDir);
|
|
||||||
if (isQlCommand) {
|
|
||||||
files = files.filter((x) => x.includes(key));
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
.map((x) => ({
|
|
||||||
filename: x,
|
|
||||||
directory: logPath,
|
|
||||||
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => b.time - a.time);
|
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getKey(command: string): string {
|
private makeCommand(tab: Crontab, realTime?: boolean) {
|
||||||
const start =
|
let command = tab.command.trim();
|
||||||
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||||
const end =
|
command = `${TASK_PREFIX}${tab.command}`;
|
||||||
command.lastIndexOf('.') !== -1
|
|
||||||
? command.lastIndexOf('.')
|
|
||||||
: command.length;
|
|
||||||
|
|
||||||
const tmpStr = command.substring(0, start - 1);
|
|
||||||
let index = 0;
|
|
||||||
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
|
|
||||||
index = tmpStr.lastIndexOf('/');
|
|
||||||
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
|
|
||||||
index = tmpStr.lastIndexOf(':');
|
|
||||||
}
|
}
|
||||||
if (index) {
|
let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${
|
||||||
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
|
tab.id
|
||||||
} else {
|
} `;
|
||||||
return command.substring(start, end);
|
if (tab.task_before) {
|
||||||
|
commandVariable += `task_before='${tab.task_before
|
||||||
|
.replace(/'/g, "'\\''")
|
||||||
|
.replace(/;? *\n/g, ';')
|
||||||
|
.trim()}' `;
|
||||||
|
}
|
||||||
|
if (tab.task_after) {
|
||||||
|
commandVariable += `task_after='${tab.task_after
|
||||||
|
.replace(/'/g, "'\\''")
|
||||||
|
.replace(/;? *\n/g, ';')
|
||||||
|
.trim()}' `;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private make_command(tab: Crontab) {
|
const crontab_job_string = `${commandVariable}${command}`;
|
||||||
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
|
||||||
return crontab_job_string;
|
return crontab_job_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async set_crontab(needReloadSchedule: boolean = false) {
|
private async set_crontab(data?: { data: Crontab[]; total: number }) {
|
||||||
const tabs = await this.crontabs();
|
const tabs = data ?? (await this.crontabs());
|
||||||
var crontab_string = '';
|
var crontab_string = '';
|
||||||
tabs.data.forEach((tab) => {
|
tabs.data.forEach((tab) => {
|
||||||
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||||
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
|
if (
|
||||||
|
tab.isDisabled === 1 ||
|
||||||
|
_schedule!.length !== 5 ||
|
||||||
|
tab.extra_schedules?.length
|
||||||
|
) {
|
||||||
crontab_string += '# ';
|
crontab_string += '# ';
|
||||||
crontab_string += tab.schedule;
|
crontab_string += tab.schedule;
|
||||||
crontab_string += ' ';
|
crontab_string += ' ';
|
||||||
crontab_string += this.make_command(tab);
|
crontab_string += this.makeCommand(tab);
|
||||||
crontab_string += '\n';
|
crontab_string += '\n';
|
||||||
} else {
|
} else {
|
||||||
crontab_string += tab.schedule;
|
crontab_string += tab.schedule;
|
||||||
crontab_string += ' ';
|
crontab_string += ' ';
|
||||||
crontab_string += this.make_command(tab);
|
crontab_string += this.makeCommand(tab);
|
||||||
crontab_string += '\n';
|
crontab_string += '\n';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.silly(crontab_string);
|
await writeFileWithLock(config.crontabFile, crontab_string);
|
||||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
|
||||||
|
|
||||||
execSync(`crontab ${config.crontabFile}`);
|
execSync(`crontab ${config.crontabFile}`);
|
||||||
if (needReloadSchedule) {
|
|
||||||
exec(`pm2 reload schedule`);
|
|
||||||
}
|
|
||||||
await CrontabModel.update({ saved: true }, { where: {} });
|
await CrontabModel.update({ saved: true }, { where: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +642,19 @@ export default class CronService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public autosave_crontab() {
|
public async autosave_crontab() {
|
||||||
return this.set_crontab();
|
const tabs = await this.crontabs();
|
||||||
|
this.set_crontab(tabs);
|
||||||
|
|
||||||
|
const sixCron = tabs.data
|
||||||
|
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
|
||||||
|
.map((doc) => ({
|
||||||
|
name: doc.name || '',
|
||||||
|
id: String(doc.id),
|
||||||
|
schedule: doc.schedule!,
|
||||||
|
command: this.makeCommand(doc),
|
||||||
|
extraSchedules: doc.extra_schedules || [],
|
||||||
|
}));
|
||||||
|
await cronClient.addCron(sixCron);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-10
@@ -1,14 +1,20 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
||||||
import { initEnvPosition } from '../data/env';
|
import {
|
||||||
|
initPosition,
|
||||||
|
maxPosition,
|
||||||
|
minPosition,
|
||||||
|
stepPosition,
|
||||||
|
} from '../data/env';
|
||||||
|
import { FindOptions } from 'sequelize';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronViewService {
|
export default class CronViewService {
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
public async create(payload: CrontabView): Promise<CrontabView> {
|
public async create(payload: CrontabView): Promise<CrontabView> {
|
||||||
let position = initEnvPosition;
|
let position = initPosition;
|
||||||
const views = await this.list();
|
const views = await this.list();
|
||||||
if (views && views.length > 0 && views[views.length - 1].position) {
|
if (views && views.length > 0 && views[views.length - 1].position) {
|
||||||
position = views[views.length - 1].position as number;
|
position = views[views.length - 1].position as number;
|
||||||
@@ -16,6 +22,8 @@ export default class CronViewService {
|
|||||||
position = position / 2;
|
position = position / 2;
|
||||||
const tab = new CrontabView({ ...payload, position });
|
const tab = new CrontabView({ ...payload, position });
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
|
|
||||||
|
await this.checkPosition(tab.position!);
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +32,9 @@ export default class CronViewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: CrontabView): Promise<CrontabView> {
|
public async update(payload: CrontabView): Promise<CrontabView> {
|
||||||
const newDoc = await this.updateDb(payload);
|
const doc = await this.getDb({ id: payload.id });
|
||||||
|
const tab = new CrontabView({ ...doc, ...payload });
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +59,14 @@ export default class CronViewService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<CrontabView> {
|
public async getDb(
|
||||||
|
query: FindOptions<CrontabView>['where'],
|
||||||
|
): Promise<CrontabView> {
|
||||||
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
|
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as CrontabView);
|
if (!doc) {
|
||||||
|
throw new Error(`CronView ${JSON.stringify(query)} not found`);
|
||||||
|
}
|
||||||
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
public async disabled(ids: number[]) {
|
||||||
@@ -62,6 +77,22 @@ export default class CronViewService {
|
|||||||
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkPosition(position: number) {
|
||||||
|
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||||
|
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
|
||||||
|
const envs = await this.list();
|
||||||
|
let position = initPosition;
|
||||||
|
for (const env of envs) {
|
||||||
|
position = position - stepPosition;
|
||||||
|
await this.updateDb({ id: env.id, position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPrecisionPosition(position: number): number {
|
||||||
|
return parseFloat(position.toPrecision(16));
|
||||||
|
}
|
||||||
|
|
||||||
public async move({
|
public async move({
|
||||||
id,
|
id,
|
||||||
fromIndex,
|
fromIndex,
|
||||||
@@ -76,17 +107,19 @@ export default class CronViewService {
|
|||||||
const views = await this.list();
|
const views = await this.list();
|
||||||
if (toIndex === 0 || toIndex === views.length - 1) {
|
if (toIndex === 0 || toIndex === views.length - 1) {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? views[0].position * 2
|
? views[0].position! * 2
|
||||||
: views[toIndex].position / 2;
|
: views[toIndex].position! / 2;
|
||||||
} else {
|
} else {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? (views[toIndex].position + views[toIndex - 1].position) / 2
|
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
|
||||||
: (views[toIndex].position + views[toIndex + 1].position) / 2;
|
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
|
||||||
}
|
}
|
||||||
const newDoc = await this.update({
|
const newDoc = await this.update({
|
||||||
id,
|
id,
|
||||||
position: targetPosition,
|
position: this.getPrecisionPosition(targetPosition),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.checkPosition(targetPosition);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+252
-110
@@ -8,13 +8,20 @@ import {
|
|||||||
DependenceTypes,
|
DependenceTypes,
|
||||||
unInstallDependenceCommandTypes,
|
unInstallDependenceCommandTypes,
|
||||||
DependenceModel,
|
DependenceModel,
|
||||||
|
GetDependenceCommandTypes,
|
||||||
|
versionDependenceCommandTypes,
|
||||||
} from '../data/dependence';
|
} from '../data/dependence';
|
||||||
import _ from 'lodash';
|
import { spawn } from 'cross-spawn';
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import { Op } from 'sequelize';
|
import { FindOptions, Op } from 'sequelize';
|
||||||
import { concurrentRun } from '../config/util';
|
import {
|
||||||
|
fileExist,
|
||||||
|
getPid,
|
||||||
|
killTask,
|
||||||
|
promiseExecSuccess,
|
||||||
|
} from '../config/util';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class DependenceService {
|
export default class DependenceService {
|
||||||
@@ -25,7 +32,7 @@ export default class DependenceService {
|
|||||||
|
|
||||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
const tabs = payloads.map((x) => {
|
const tabs = payloads.map((x) => {
|
||||||
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
|
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
|
||||||
return tab;
|
return tab;
|
||||||
});
|
});
|
||||||
const docs = await this.insert(tabs);
|
const docs = await this.insert(tabs);
|
||||||
@@ -46,7 +53,7 @@ export default class DependenceService {
|
|||||||
const tab = new Dependence({
|
const tab = new Dependence({
|
||||||
...doc,
|
...doc,
|
||||||
...other,
|
...other,
|
||||||
status: DependenceStatus.installing,
|
status: DependenceStatus.queued,
|
||||||
});
|
});
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
this.installDependenceOneByOne([newDoc]);
|
this.installDependenceOneByOne([newDoc]);
|
||||||
@@ -59,12 +66,23 @@ export default class DependenceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
||||||
await DependenceModel.update(
|
|
||||||
{ status: DependenceStatus.removing, log: [] },
|
|
||||||
{ where: { id: ids } },
|
|
||||||
);
|
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
this.installDependenceOneByOne(docs, false, force);
|
const unInstalledDeps = docs.filter(
|
||||||
|
(x) => x.status !== DependenceStatus.installed,
|
||||||
|
);
|
||||||
|
const installedDeps = docs.filter(
|
||||||
|
(x) => x.status === DependenceStatus.installed,
|
||||||
|
);
|
||||||
|
await this.removeDb(unInstalledDeps.map((x) => x.id!));
|
||||||
|
|
||||||
|
if (installedDeps.length) {
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.queued, log: [] },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
|
||||||
|
this.installDependenceOneByOne(docs, false, force);
|
||||||
|
}
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,13 +91,23 @@ export default class DependenceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async dependencies(
|
public async dependencies(
|
||||||
{ searchValue, type }: { searchValue: string; type: string },
|
{
|
||||||
sort: any = { position: -1 },
|
searchValue,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
}: { searchValue: string; type: string; status: string },
|
||||||
|
sort: any = [],
|
||||||
query: any = {},
|
query: any = {},
|
||||||
): Promise<Dependence[]> {
|
): Promise<Dependence[]> {
|
||||||
let condition = { ...query, type: DependenceTypes[type as any] };
|
let condition = {
|
||||||
|
...query,
|
||||||
|
type: DependenceTypes[type as any],
|
||||||
|
};
|
||||||
|
if (status) {
|
||||||
|
condition.status = status.split(',').map(Number);
|
||||||
|
}
|
||||||
if (searchValue) {
|
if (searchValue) {
|
||||||
const encodeText = encodeURIComponent(searchValue);
|
const encodeText = encodeURI(searchValue);
|
||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchValue}%` },
|
{ [Op.like]: `%${searchValue}%` },
|
||||||
@@ -93,38 +121,56 @@ export default class DependenceService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition);
|
const result = await this.find(condition, sort);
|
||||||
return result as any;
|
return result as any;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private installDependenceOneByOne(
|
public installDependenceOneByOne(
|
||||||
docs: Dependence[],
|
docs: Dependence[],
|
||||||
isInstall: boolean = true,
|
isInstall: boolean = true,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
) {
|
) {
|
||||||
concurrentRun(
|
docs.forEach((dep) => {
|
||||||
docs.map(
|
this.installOrUninstallDependency(dep, isInstall, force);
|
||||||
(dep) => async () =>
|
});
|
||||||
await this.installOrUninstallDependencies([dep], isInstall, force),
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
||||||
await DependenceModel.update(
|
await DependenceModel.update(
|
||||||
{ status: DependenceStatus.installing, log: [] },
|
{ status: DependenceStatus.queued, log: [] },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
|
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
this.installDependenceOneByOne(docs);
|
this.installDependenceOneByOne(docs, true, true);
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async cancel(ids: number[]) {
|
||||||
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
taskLimit.removeQueuedDependency(doc);
|
||||||
|
const depInstallCommand = InstallDependenceCommandTypes[doc.type];
|
||||||
|
const depUnInstallCommand = unInstallDependenceCommandTypes[doc.type];
|
||||||
|
const installCmd = `${depInstallCommand} ${doc.name.trim()}`;
|
||||||
|
const unInstallCmd = `${depUnInstallCommand} ${doc.name.trim()}`;
|
||||||
|
const pids = await Promise.all([
|
||||||
|
getPid(installCmd),
|
||||||
|
getPid(unInstallCmd),
|
||||||
|
]);
|
||||||
|
for (const pid of pids) {
|
||||||
|
pid && (await killTask(pid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.cancelled },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async find(query: any, sort: any = []): Promise<Dependence[]> {
|
private async find(query: any, sort: any = []): Promise<Dependence[]> {
|
||||||
const docs = await DependenceModel.findAll({
|
const docs = await DependenceModel.findAll({
|
||||||
where: { ...query },
|
where: { ...query },
|
||||||
@@ -133,113 +179,209 @@ export default class DependenceService {
|
|||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<Dependence> {
|
public async getDb(
|
||||||
|
query: FindOptions<Dependence>['where'],
|
||||||
|
): Promise<Dependence> {
|
||||||
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as Dependence);
|
if (!doc) {
|
||||||
|
throw new Error(`Dependency ${JSON.stringify(query)} not found`);
|
||||||
|
}
|
||||||
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateLog(ids: number[], log: string): Promise<void> {
|
private async updateLog(ids: number[], log: string): Promise<void> {
|
||||||
const doc = await DependenceModel.findOne({ where: { id: ids } });
|
taskLimit.updateDepLog(async () => {
|
||||||
const newLog = doc?.log ? [...doc.log, log] : [log];
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
|
for (const doc of docs) {
|
||||||
|
const newLog = doc?.log ? [...doc.log, log] : [log];
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ log: newLog },
|
||||||
|
{ where: { id: doc.id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public installOrUninstallDependencies(
|
public installOrUninstallDependency(
|
||||||
dependencies: Dependence[],
|
dependency: Dependence,
|
||||||
isInstall: boolean = true,
|
isInstall: boolean = true,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
) {
|
) {
|
||||||
return new Promise(async (resolve) => {
|
return taskLimit.runDependeny(dependency, () => {
|
||||||
if (dependencies.length === 0) {
|
return new Promise(async (resolve) => {
|
||||||
resolve(null);
|
if (taskLimit.firstDependencyId !== dependency.id) {
|
||||||
return;
|
return resolve(null);
|
||||||
}
|
}
|
||||||
const socketMessageType = !force
|
|
||||||
? 'installDependence'
|
|
||||||
: 'uninstallDependence';
|
|
||||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
|
||||||
const depRunCommand = (
|
|
||||||
isInstall
|
|
||||||
? InstallDependenceCommandTypes
|
|
||||||
: unInstallDependenceCommandTypes
|
|
||||||
)[dependencies[0].type as any];
|
|
||||||
const actionText = isInstall ? '安装' : '删除';
|
|
||||||
const depIds = dependencies.map((x) => x.id) as number[];
|
|
||||||
const startTime = dayjs();
|
|
||||||
|
|
||||||
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
|
taskLimit.removeQueuedDependency(dependency);
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
)}\n\n`;
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: socketMessageType,
|
|
||||||
message,
|
|
||||||
references: depIds,
|
|
||||||
});
|
|
||||||
await this.updateLog(depIds, message);
|
|
||||||
|
|
||||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
const depIds = [dependency.id!];
|
||||||
|
const status = isInstall
|
||||||
|
? DependenceStatus.installing
|
||||||
|
: DependenceStatus.removing;
|
||||||
|
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||||
|
|
||||||
cp.stdout.on('data', async (data) => {
|
const socketMessageType = isInstall
|
||||||
this.sockService.sendMessage({
|
? 'installDependence'
|
||||||
type: socketMessageType,
|
: 'uninstallDependence';
|
||||||
message: data.toString(),
|
let depName = dependency.name.trim();
|
||||||
references: depIds,
|
const depRunCommand = (
|
||||||
});
|
isInstall
|
||||||
await this.updateLog(depIds, data.toString());
|
? InstallDependenceCommandTypes
|
||||||
});
|
: unInstallDependenceCommandTypes
|
||||||
|
)[dependency.type];
|
||||||
|
const actionText = isInstall ? '安装' : '删除';
|
||||||
|
const startTime = dayjs();
|
||||||
|
|
||||||
cp.stderr.on('data', async (data) => {
|
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: socketMessageType,
|
|
||||||
message: data.toString(),
|
|
||||||
references: depIds,
|
|
||||||
});
|
|
||||||
await this.updateLog(depIds, data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('error', async (err) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: socketMessageType,
|
|
||||||
message: JSON.stringify(err),
|
|
||||||
references: depIds,
|
|
||||||
});
|
|
||||||
await this.updateLog(depIds, JSON.stringify(err));
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('close', async (code) => {
|
|
||||||
const endTime = dayjs();
|
|
||||||
const isSucceed = code === 0;
|
|
||||||
const resultText = isSucceed ? '成功' : '失败';
|
|
||||||
|
|
||||||
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
)}\n\n`;
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: socketMessageType,
|
type: socketMessageType,
|
||||||
message,
|
message,
|
||||||
references: depIds,
|
references: depIds,
|
||||||
});
|
});
|
||||||
await this.updateLog(depIds, message);
|
this.updateLog(depIds, message);
|
||||||
|
|
||||||
let status = null;
|
// 判断是否已经安装过依赖
|
||||||
if (isSucceed) {
|
if (isInstall && !force) {
|
||||||
status = isInstall
|
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
|
||||||
? DependenceStatus.installed
|
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
||||||
: DependenceStatus.removed;
|
let depVersion = '';
|
||||||
} else {
|
if (depName.includes(depVersionStr)) {
|
||||||
status = isInstall
|
const symbolRegx = new RegExp(
|
||||||
? DependenceStatus.installFailed
|
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
|
||||||
: DependenceStatus.removeFailed;
|
);
|
||||||
|
const [, _depName, _depVersion] = depName.match(symbolRegx) || [];
|
||||||
|
if (_depVersion && _depName) {
|
||||||
|
depName = _depName;
|
||||||
|
depVersion = _depVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isNodeDependence = dependency.type === DependenceTypes.nodejs;
|
||||||
|
const isLinuxDependence = dependency.type === DependenceTypes.linux;
|
||||||
|
const isPythonDependence =
|
||||||
|
dependency.type === DependenceTypes.python3;
|
||||||
|
const depInfo = (
|
||||||
|
await promiseExecSuccess(
|
||||||
|
isNodeDependence
|
||||||
|
? `${getCommandPrefix} | grep "${depName}" | head -1`
|
||||||
|
: `${getCommandPrefix} ${depName}`,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.replace(/\s{2,}/, ' ')
|
||||||
|
.replace(/\s+$/, '');
|
||||||
|
|
||||||
|
if (
|
||||||
|
depInfo &&
|
||||||
|
((isNodeDependence && depInfo.split(' ')?.[0] === depName) ||
|
||||||
|
(isLinuxDependence &&
|
||||||
|
depInfo.toLocaleLowerCase().includes('installed')) ||
|
||||||
|
isPythonDependence) &&
|
||||||
|
(!depVersion || depInfo.includes(depVersion))
|
||||||
|
) {
|
||||||
|
const endTime = dayjs();
|
||||||
|
const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: _message,
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, _message);
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.installed },
|
||||||
|
{ where: { id: depIds } },
|
||||||
|
);
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
const dependenceProxyFileExist = await fileExist(
|
||||||
|
config.dependenceProxyFile,
|
||||||
|
);
|
||||||
|
const proxyStr = dependenceProxyFileExist
|
||||||
|
? `source ${config.dependenceProxyFile} &&`
|
||||||
|
: '';
|
||||||
|
const cp = spawn(
|
||||||
|
`${proxyStr} ${depRunCommand} ${dependency.name.trim()}`,
|
||||||
|
{
|
||||||
|
shell: '/bin/bash',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 如果删除依赖成功或者强制删除
|
cp.stdout.on('data', async (data) => {
|
||||||
if ((isSucceed || force) && !isInstall) {
|
this.sockService.sendMessage({
|
||||||
this.removeDb(depIds);
|
type: socketMessageType,
|
||||||
}
|
message: data.toString(),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
resolve(null);
|
cp.stderr.on('data', async (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: data.toString(),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('error', async (err) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: JSON.stringify(err),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, JSON.stringify(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code) => {
|
||||||
|
const endTime = dayjs();
|
||||||
|
const isSucceed = code === 0;
|
||||||
|
const resultText = isSucceed ? '成功' : '失败';
|
||||||
|
|
||||||
|
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message,
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, message);
|
||||||
|
|
||||||
|
let status: number;
|
||||||
|
if (isSucceed) {
|
||||||
|
status = isInstall
|
||||||
|
? DependenceStatus.installed
|
||||||
|
: DependenceStatus.removed;
|
||||||
|
} else {
|
||||||
|
status = isInstall
|
||||||
|
? DependenceStatus.installFailed
|
||||||
|
: DependenceStatus.removeFailed;
|
||||||
|
}
|
||||||
|
const docs = await DependenceModel.findAll({ where: { id: depIds } });
|
||||||
|
const _docIds = docs
|
||||||
|
.filter((x) => x.status !== DependenceStatus.cancelled)
|
||||||
|
.map((x) => x.id!);
|
||||||
|
|
||||||
|
if (_docIds.length > 0) {
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status },
|
||||||
|
{ where: { id: _docIds } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果删除依赖成功或者强制删除
|
||||||
|
if ((isSucceed || force) && !isInstall) {
|
||||||
|
this.removeDb(depIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-47
@@ -1,11 +1,19 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { getFileContentByName } from '../config/util';
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs/promises';
|
||||||
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
|
import {
|
||||||
import _ from 'lodash';
|
Env,
|
||||||
import { Op } from 'sequelize';
|
EnvModel,
|
||||||
|
EnvStatus,
|
||||||
|
initPosition,
|
||||||
|
maxPosition,
|
||||||
|
minPosition,
|
||||||
|
stepPosition,
|
||||||
|
} from '../data/env';
|
||||||
|
import groupBy from 'lodash/groupBy';
|
||||||
|
import { FindOptions, Op } from 'sequelize';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class EnvService {
|
export default class EnvService {
|
||||||
@@ -13,22 +21,27 @@ export default class EnvService {
|
|||||||
|
|
||||||
public async create(payloads: Env[]): Promise<Env[]> {
|
public async create(payloads: Env[]): Promise<Env[]> {
|
||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
let position = initEnvPosition;
|
let position = initPosition;
|
||||||
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
|
if (
|
||||||
position = envs[envs.length - 1].position as number;
|
envs &&
|
||||||
|
envs.length > 0 &&
|
||||||
|
typeof envs[envs.length - 1].position === 'number'
|
||||||
|
) {
|
||||||
|
position = envs[envs.length - 1].position!;
|
||||||
}
|
}
|
||||||
const tabs = payloads.map((x) => {
|
const tabs = payloads.map((x) => {
|
||||||
position = position / 2;
|
position = position - stepPosition;
|
||||||
const tab = new Env({ ...x, position });
|
const tab = new Env({ ...x, position });
|
||||||
return tab;
|
return tab;
|
||||||
});
|
});
|
||||||
const docs = await this.insert(tabs);
|
const docs = await this.insert(tabs);
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
|
await this.checkPosition(tabs[tabs.length - 1].position!);
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payloads: Env[]): Promise<Env[]> {
|
public async insert(payloads: Env[]): Promise<Env[]> {
|
||||||
const result = [];
|
const result: Env[] = [];
|
||||||
for (const env of payloads) {
|
for (const env of payloads) {
|
||||||
const doc = await EnvModel.create(env, { returning: true });
|
const doc = await EnvModel.create(env, { returning: true });
|
||||||
result.push(doc);
|
result.push(doc);
|
||||||
@@ -37,7 +50,9 @@ export default class EnvService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Env): Promise<Env> {
|
public async update(payload: Env): Promise<Env> {
|
||||||
const newDoc = await this.updateDb(payload);
|
const doc = await this.getDb({ id: payload.id });
|
||||||
|
const tab = new Env({ ...doc, ...payload });
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
@@ -47,7 +62,7 @@ export default class EnvService {
|
|||||||
return await this.getDb({ id: payload.id });
|
return await this.getDb({ id: payload.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: string[]) {
|
public async remove(ids: number[]) {
|
||||||
await EnvModel.destroy({ where: { id: ids } });
|
await EnvModel.destroy({ where: { id: ids } });
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
@@ -67,28 +82,47 @@ export default class EnvService {
|
|||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
if (toIndex === 0 || toIndex === envs.length - 1) {
|
if (toIndex === 0 || toIndex === envs.length - 1) {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? envs[0].position * 2
|
? envs[0].position! + stepPosition
|
||||||
: envs[toIndex].position / 2;
|
: envs[toIndex].position! - stepPosition;
|
||||||
} else {
|
} else {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
|
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
|
||||||
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDoc = await this.update({
|
const newDoc = await this.update({
|
||||||
id,
|
id,
|
||||||
position: targetPosition,
|
position: this.getPrecisionPosition(targetPosition),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.checkPosition(targetPosition, envs[toIndex].position!);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async envs(
|
private async checkPosition(position: number, edge: number = 0) {
|
||||||
searchText: string = '',
|
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||||
sort: any = { position: -1 },
|
if (
|
||||||
query: any = {},
|
precisionPosition < minPosition ||
|
||||||
): Promise<Env[]> {
|
precisionPosition > maxPosition ||
|
||||||
|
Math.abs(precisionPosition - edge) < minPosition
|
||||||
|
) {
|
||||||
|
const envs = await this.envs();
|
||||||
|
let position = initPosition;
|
||||||
|
for (const env of envs) {
|
||||||
|
position = position - stepPosition;
|
||||||
|
await this.updateDb({ id: env.id, position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPrecisionPosition(position: number): number {
|
||||||
|
return parseFloat(position.toPrecision(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
|
||||||
let condition = { ...query };
|
let condition = { ...query };
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const encodeText = encodeURIComponent(searchText);
|
const encodeText = encodeURI(searchText);
|
||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchText}%` },
|
{ [Op.like]: `%${searchText}%` },
|
||||||
@@ -113,11 +147,10 @@ export default class EnvService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition, [
|
const result = await this.find(condition, [
|
||||||
['status', 'ASC'],
|
|
||||||
['position', 'DESC'],
|
['position', 'DESC'],
|
||||||
['createdAt', 'DESC'],
|
['createdAt', 'ASC'],
|
||||||
]);
|
]);
|
||||||
return result as any;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -128,15 +161,18 @@ export default class EnvService {
|
|||||||
where: { ...query },
|
where: { ...query },
|
||||||
order: [...sort],
|
order: [...sort],
|
||||||
});
|
});
|
||||||
return docs;
|
return docs.map((x) => x.get({ plain: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<Env> {
|
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
||||||
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as Env);
|
if (!doc) {
|
||||||
|
throw new Error(`Env ${JSON.stringify(query)} not found`);
|
||||||
|
}
|
||||||
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: string[]) {
|
public async disabled(ids: number[]) {
|
||||||
await EnvModel.update(
|
await EnvModel.update(
|
||||||
{ status: EnvStatus.disabled },
|
{ status: EnvStatus.disabled },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
@@ -144,42 +180,54 @@ export default class EnvService {
|
|||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: string[]) {
|
public async enabled(ids: number[]) {
|
||||||
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
public async updateNames({ ids, name }: { ids: number[]; name: string }) {
|
||||||
await EnvModel.update({ name }, { where: { id: ids } });
|
await EnvModel.update({ name }, { where: { id: ids } });
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async set_envs() {
|
public async set_envs() {
|
||||||
const envs = await this.envs(
|
const envs = await this.envs('', {
|
||||||
'',
|
name: { [Op.not]: null },
|
||||||
{ position: -1 },
|
status: EnvStatus.normal,
|
||||||
{ name: { [Op.not]: null } },
|
});
|
||||||
);
|
const groups = groupBy(envs, 'name');
|
||||||
const groups = _.groupBy(envs, 'name');
|
|
||||||
let env_string = '';
|
let env_string = '';
|
||||||
|
let js_env_string = '';
|
||||||
|
let py_env_string = 'import os\n';
|
||||||
for (const key in groups) {
|
for (const key in groups) {
|
||||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||||
const group = groups[key];
|
const group = groups[key];
|
||||||
|
|
||||||
// 忽略不符合bash要求的环境变量名称
|
// 忽略不符合bash要求的环境变量名称
|
||||||
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
|
||||||
let value = _(group)
|
let value = group
|
||||||
.filter((x) => x.status !== EnvStatus.disabled)
|
.map((x) => x.value)
|
||||||
.map('value')
|
|
||||||
.join('&')
|
.join('&')
|
||||||
.replace(/(\\)[^n]/g, '\\\\')
|
.replace(/'/g, "'\\''")
|
||||||
.replace(/(\\$)/, '\\\\')
|
|
||||||
.replace(/"/g, '\\"')
|
|
||||||
.trim();
|
.trim();
|
||||||
env_string += `export ${key}="${value}"\n`;
|
env_string += `export ${key}='${value}'\n`;
|
||||||
|
const _env_value = `${group
|
||||||
|
.map((x) => x.value)
|
||||||
|
.join('&')
|
||||||
|
.replace(/\\/g, '\\\\')}`;
|
||||||
|
js_env_string += `process.env.${key}=\`${_env_value.replace(
|
||||||
|
/\`/g,
|
||||||
|
'\\`',
|
||||||
|
)}\`;\n`;
|
||||||
|
py_env_string += `os.environ['${key}']='''${_env_value.replace(
|
||||||
|
/\'/g,
|
||||||
|
"\\'",
|
||||||
|
)}'''\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fs.writeFileSync(config.envFile, env_string);
|
await writeFileWithLock(config.envFile, env_string);
|
||||||
|
await writeFileWithLock(config.jsEnvFile, js_env_string);
|
||||||
|
await writeFileWithLock(config.pyEnvFile, py_env_string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+611
-196
@@ -1,12 +1,11 @@
|
|||||||
import { NotificationInfo } from '../data/notify';
|
|
||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import UserService from './user';
|
|
||||||
import got from 'got';
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import got from 'got';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import { parseBody, parseHeaders } from '../config/util';
|
import { parseBody, parseHeaders } from '../config/util';
|
||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import UserService from './user';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class NotificationService {
|
export default class NotificationService {
|
||||||
@@ -24,18 +23,28 @@ export default class NotificationService {
|
|||||||
['dingtalkBot', this.dingtalkBot],
|
['dingtalkBot', this.dingtalkBot],
|
||||||
['weWorkBot', this.weWorkBot],
|
['weWorkBot', this.weWorkBot],
|
||||||
['weWorkApp', this.weWorkApp],
|
['weWorkApp', this.weWorkApp],
|
||||||
|
['aibotk', this.aibotk],
|
||||||
['iGot', this.iGot],
|
['iGot', this.iGot],
|
||||||
['pushPlus', this.pushPlus],
|
['pushPlus', this.pushPlus],
|
||||||
|
['wePlusBot', this.wePlusBot],
|
||||||
['email', this.email],
|
['email', this.email],
|
||||||
|
['pushMe', this.pushMe],
|
||||||
['webhook', this.webhook],
|
['webhook', this.webhook],
|
||||||
|
['lark', this.lark],
|
||||||
|
['chronocat', this.chronocat],
|
||||||
|
['ntfy', this.ntfy],
|
||||||
|
['wxPusherBot', this.wxPusherBot],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
private timeout = 10000;
|
|
||||||
private title = '';
|
private title = '';
|
||||||
private content = '';
|
private content = '';
|
||||||
private params!: Omit<NotificationInfo, 'type'>;
|
private params!: Omit<NotificationInfo, 'type'>;
|
||||||
|
private gotOption = {
|
||||||
|
timeout: 10000,
|
||||||
|
retry: 1,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
constructor() {}
|
||||||
|
|
||||||
public async notify(
|
public async notify(
|
||||||
title: string,
|
title: string,
|
||||||
@@ -50,7 +59,7 @@ export default class NotificationService {
|
|||||||
try {
|
try {
|
||||||
return await notificationModeAction?.call(this);
|
return await notificationModeAction?.call(this);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return false;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -73,103 +82,166 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async gotify() {
|
private async gotify() {
|
||||||
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
const res: any = await got
|
||||||
timeout: this.timeout,
|
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||||
retry: 0,
|
...this.gotOption,
|
||||||
body: `title=${encodeURIComponent(
|
body: `title=${encodeURIComponent(
|
||||||
this.title,
|
this.title,
|
||||||
)}&message=${encodeURIComponent(
|
)}&message=${encodeURIComponent(
|
||||||
this.content,
|
this.content,
|
||||||
)}&priority=${gotifyPriority}`,
|
)}&priority=${gotifyPriority}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return typeof res.id === 'number';
|
if (typeof res.id === 'number') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async goCqHttpBot() {
|
private async goCqHttpBot() {
|
||||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
const res: any = await got
|
||||||
timeout: this.timeout,
|
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||||
retry: 0,
|
...this.gotOption,
|
||||||
json: { message: `${this.title}\n${this.content}` },
|
json: { message: `${this.title}\n${this.content}` },
|
||||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.retcode === 0;
|
if (res.retcode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async serverChan() {
|
private async serverChan() {
|
||||||
const { serverChanKey } = this.params;
|
const { serverChanKey } = this.params;
|
||||||
const url = serverChanKey.startsWith('SCT')
|
const matchResult = serverChanKey.match(/^sctp(\d+)t/i);
|
||||||
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
const url =
|
||||||
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
matchResult && matchResult[1]
|
||||||
const res: any = await got
|
? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send`
|
||||||
.post(url, {
|
: `https://sctapi.ftqq.com/${serverChanKey}.send`;
|
||||||
timeout: this.timeout,
|
|
||||||
retry: 0,
|
try {
|
||||||
body: `title=${this.title}&desp=${this.content}`,
|
const res: any = await got
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
.post(url, {
|
||||||
})
|
...this.gotOption,
|
||||||
.json();
|
body: `title=${encodeURIComponent(
|
||||||
return res.errno === 0 || res.data.errno === 0;
|
this.title,
|
||||||
|
)}&desp=${encodeURIComponent(this.content)}`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
if (res.errno === 0 || res.data.errno === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pushDeer() {
|
private async pushDeer() {
|
||||||
const { pushDeerKey } = this.params;
|
const { pushDeerKey, pushDeerUrl } = this.params;
|
||||||
const url = `https://api2.pushdeer.com/message/push`;
|
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
timeout: this.timeout,
|
.post(url, {
|
||||||
retry: 0,
|
...this.gotOption,
|
||||||
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
||||||
this.title,
|
this.title,
|
||||||
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return (
|
if (
|
||||||
res.content.result.length !== undefined && res.content.result.length > 0
|
res.content.result.length !== undefined &&
|
||||||
);
|
res.content.result.length > 0
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async chat() {
|
private async chat() {
|
||||||
const { chatUrl, chatToken } = this.params;
|
const { chatUrl, chatToken } = this.params;
|
||||||
const url = `${chatUrl}${chatToken}`;
|
const url = `${chatUrl}${chatToken}`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
timeout: this.timeout,
|
.post(url, {
|
||||||
retry: 0,
|
...this.gotOption,
|
||||||
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.success;
|
if (res.success) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async bark() {
|
private async bark() {
|
||||||
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
let {
|
||||||
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
barkPush,
|
||||||
|
barkIcon = '',
|
||||||
|
barkSound = '',
|
||||||
|
barkGroup = '',
|
||||||
|
barkLevel = '',
|
||||||
|
barkUrl = '',
|
||||||
|
barkArchive = '',
|
||||||
|
} = this.params;
|
||||||
|
if (!barkPush.startsWith('http')) {
|
||||||
barkPush = `https://api.day.app/${barkPush}`;
|
barkPush = `https://api.day.app/${barkPush}`;
|
||||||
}
|
}
|
||||||
const url = `${barkPush}/${encodeURIComponent(
|
const url = `${barkPush}`;
|
||||||
this.title,
|
const body = {
|
||||||
)}/${encodeURIComponent(
|
title: this.title,
|
||||||
this.content,
|
body: this.content,
|
||||||
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
|
icon: barkIcon,
|
||||||
const res: any = await got
|
sound: barkSound,
|
||||||
.get(url, {
|
group: barkGroup,
|
||||||
timeout: this.timeout,
|
isArchive: barkArchive,
|
||||||
retry: 0,
|
level: barkLevel,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
url: barkUrl,
|
||||||
})
|
};
|
||||||
.json();
|
try {
|
||||||
return res.code === 200;
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: body,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
if (res.code === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async telegramBot() {
|
private async telegramBot() {
|
||||||
@@ -182,8 +254,9 @@ export default class NotificationService {
|
|||||||
telegramBotUserId,
|
telegramBotUserId,
|
||||||
} = this.params;
|
} = this.params;
|
||||||
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
|
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
|
||||||
const url = `https://${telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
|
const url = `${
|
||||||
}/bot${telegramBotToken}/sendMessage`;
|
telegramBotApiHost ? telegramBotApiHost : 'https://api.telegram.org'
|
||||||
|
}/bot${telegramBotToken}/sendMessage`;
|
||||||
let agent;
|
let agent;
|
||||||
if (telegramBotProxyHost && telegramBotProxyPort) {
|
if (telegramBotProxyHost && telegramBotProxyPort) {
|
||||||
const options: any = {
|
const options: any = {
|
||||||
@@ -200,16 +273,23 @@ export default class NotificationService {
|
|||||||
https: httpsAgent,
|
https: httpsAgent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
timeout: this.timeout,
|
.post(url, {
|
||||||
retry: 0,
|
...this.gotOption,
|
||||||
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return !!res.ok;
|
if (res.ok) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async dingtalkBot() {
|
private async dingtalkBot() {
|
||||||
@@ -223,48 +303,63 @@ export default class NotificationService {
|
|||||||
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
||||||
}
|
}
|
||||||
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
timeout: this.timeout,
|
.post(url, {
|
||||||
retry: 0,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
content: ` ${this.title}\n\n${this.content}`,
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
.json();
|
||||||
.json();
|
if (res.errcode === 0) {
|
||||||
return res.errcode === 0;
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async weWorkBot() {
|
private async weWorkBot() {
|
||||||
const { weWorkBotKey } = this.params;
|
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
||||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
this.params;
|
||||||
const res: any = await got
|
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||||
.post(url, {
|
try {
|
||||||
timeout: this.timeout,
|
const res: any = await got
|
||||||
retry: 0,
|
.post(url, {
|
||||||
json: {
|
...this.gotOption,
|
||||||
msgtype: 'text',
|
json: {
|
||||||
text: {
|
msgtype: 'text',
|
||||||
content: ` ${this.title}\n\n${this.content}`,
|
text: {
|
||||||
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
.json();
|
||||||
.json();
|
if (res.errcode === 0) {
|
||||||
return res.errcode === 0;
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async weWorkApp() {
|
private async weWorkApp() {
|
||||||
const { weWorkAppKey } = this.params;
|
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
||||||
|
this.params;
|
||||||
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||||
weWorkAppKey.split(',');
|
weWorkAppKey.split(',');
|
||||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
|
||||||
const tokenRes: any = await got
|
const tokenRes: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
json: {
|
json: {
|
||||||
corpid,
|
corpid,
|
||||||
corpsecret,
|
corpsecret,
|
||||||
@@ -310,102 +405,432 @@ export default class NotificationService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res: any = await got
|
try {
|
||||||
.post(
|
const res: any = await got
|
||||||
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
.post(
|
||||||
{
|
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
||||||
timeout: this.timeout,
|
{
|
||||||
retry: 0,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
touser,
|
touser,
|
||||||
agentid,
|
agentid,
|
||||||
safe: '0',
|
safe: '0',
|
||||||
...options,
|
...options,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
)
|
||||||
)
|
.json();
|
||||||
.json();
|
|
||||||
|
|
||||||
return res.errcode === 0;
|
if (res.errcode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async aibotk() {
|
||||||
|
const { aibotkKey, aibotkType, aibotkName } = this.params;
|
||||||
|
let url = '';
|
||||||
|
let json = {};
|
||||||
|
switch (aibotkType) {
|
||||||
|
case 'room':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
|
||||||
|
json = {
|
||||||
|
apiKey: `${aibotkKey}`,
|
||||||
|
roomName: `${aibotkName}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'contact':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
|
||||||
|
json = {
|
||||||
|
apiKey: `${aibotkKey}`,
|
||||||
|
name: `${aibotkName}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
...json,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
if (res.code === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async iGot() {
|
private async iGot() {
|
||||||
const { iGotPushKey } = this.params;
|
const { iGotPushKey } = this.params;
|
||||||
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
timeout: this.timeout,
|
.post(url, {
|
||||||
retry: 0,
|
...this.gotOption,
|
||||||
body: `title=${this.title}&content=${this.content}`,
|
body: `title=${this.title}&content=${this.content}`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
return res.ret === 0;
|
if (res.ret === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pushPlus() {
|
private async pushPlus() {
|
||||||
const { pushPlusToken, pushPlusUser } = this.params;
|
const {
|
||||||
|
pushPlusToken,
|
||||||
|
pushPlusUser,
|
||||||
|
pushplusWebhook,
|
||||||
|
pushPlusTemplate,
|
||||||
|
pushplusChannel,
|
||||||
|
pushplusCallbackUrl,
|
||||||
|
pushplusTo,
|
||||||
|
} = this.params;
|
||||||
const url = `https://www.pushplus.plus/send`;
|
const url = `https://www.pushplus.plus/send`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
let body = {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
json: {
|
json: {
|
||||||
token: `${pushPlusToken}`,
|
token: `${pushPlusToken}`,
|
||||||
title: `${this.title}`,
|
title: `${this.title}`,
|
||||||
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||||
topic: `${pushPlusUser || ''}`,
|
topic: `${pushPlusUser || ''}`,
|
||||||
|
template: `${pushPlusTemplate || 'html'}`,
|
||||||
|
channel: `${pushplusChannel || 'wechat'}`,
|
||||||
|
webhook: `${pushplusWebhook || ''}`,
|
||||||
|
callbackUrl: `${pushplusCallbackUrl || ''}`,
|
||||||
|
to: `${pushplusTo || ''}`,
|
||||||
},
|
},
|
||||||
})
|
};
|
||||||
.json();
|
|
||||||
|
|
||||||
return res.code === 200;
|
const res: any = await got.post(url, body).json();
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async wePlusBot() {
|
||||||
|
const { wePlusBotToken, wePlusBotReceiver, wePlusBotVersion } = this.params;
|
||||||
|
|
||||||
|
let content = this.content;
|
||||||
|
let template = 'txt';
|
||||||
|
if (this.content.length > 800) {
|
||||||
|
template = 'html';
|
||||||
|
content = content.replace(/[\n\r]/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://www.weplusbot.com/send`;
|
||||||
|
try {
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
token: `${wePlusBotToken}`,
|
||||||
|
title: `${this.title}`,
|
||||||
|
template: `${template}`,
|
||||||
|
content: `${content}`,
|
||||||
|
receiver: `${wePlusBotReceiver || ''}`,
|
||||||
|
version: `${wePlusBotVersion || 'pro'}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async lark() {
|
||||||
|
let { larkKey } = this.params;
|
||||||
|
|
||||||
|
if (!larkKey.startsWith('http')) {
|
||||||
|
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await got
|
||||||
|
.post(larkKey, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
msg_type: 'text',
|
||||||
|
content: { text: `${this.title}\n\n${this.content}` },
|
||||||
|
},
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
if (res.StatusCode === 0 || res.code === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async email() {
|
private async email() {
|
||||||
const { emailPass, emailService, emailUser } = this.params;
|
const { emailPass, emailService, emailUser } = this.params;
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
service: emailService,
|
|
||||||
auth: {
|
|
||||||
user: emailUser,
|
|
||||||
pass: emailPass,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const info = await transporter.sendMail({
|
try {
|
||||||
from: `"青龙快讯" <${emailUser}>`,
|
const transporter = nodemailer.createTransport({
|
||||||
to: `${emailUser}`,
|
service: emailService,
|
||||||
subject: `${this.title}`,
|
auth: {
|
||||||
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
user: emailUser,
|
||||||
});
|
pass: emailPass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
transporter.close();
|
const info = await transporter.sendMail({
|
||||||
|
from: `"青龙快讯" <${emailUser}>`,
|
||||||
|
to: `${emailUser}`,
|
||||||
|
subject: `${this.title}`,
|
||||||
|
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||||
|
});
|
||||||
|
|
||||||
return !!info.messageId;
|
transporter.close();
|
||||||
|
|
||||||
|
if (info.messageId) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(info));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pushMe() {
|
||||||
|
const { pushMeKey, pushMeUrl } = this.params;
|
||||||
|
try {
|
||||||
|
const res: any = await got.post(pushMeUrl || 'https://push.i-i.me/', {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
push_key: pushMeKey,
|
||||||
|
title: this.title,
|
||||||
|
content: this.content,
|
||||||
|
},
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
if (res.body === 'success') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.body);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ntfy() {
|
||||||
|
const { ntfyUrl, ntfyTopic, ntfyPriority } = this.params;
|
||||||
|
// 编码函数
|
||||||
|
const encodeRfc2047 = (text: string, charset: string = 'UTF-8'): string => {
|
||||||
|
const encodedText = Buffer.from(text).toString('base64');
|
||||||
|
return `=?${charset}?B?${encodedText}?=`;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const encodedTitle = encodeRfc2047(this.title);
|
||||||
|
const res: any = await got.post(
|
||||||
|
`${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`,
|
||||||
|
{
|
||||||
|
...this.gotOption,
|
||||||
|
body: `${this.content}`,
|
||||||
|
headers: { Title: encodedTitle, Priority: `${ntfyPriority || '3'}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async wxPusherBot() {
|
||||||
|
const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } =
|
||||||
|
this.params;
|
||||||
|
// 处理 topicIds,将分号分隔的字符串转为数组
|
||||||
|
const topicIds = wxPusherBotTopicIds
|
||||||
|
? wxPusherBotTopicIds
|
||||||
|
.split(';')
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter((id) => id)
|
||||||
|
.map((id) => parseInt(id))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// 处理 uids,将分号分隔的字符串转为数组
|
||||||
|
const uids = wxPusherBotUids
|
||||||
|
? wxPusherBotUids
|
||||||
|
.split(';')
|
||||||
|
.map((uid) => uid.trim())
|
||||||
|
.filter((uid) => uid)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// topic_ids 和 uids 至少要有一个
|
||||||
|
if (!topicIds.length && !uids.length) {
|
||||||
|
throw new Error('wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://wxpusher.zjiecode.com/api/send/message`;
|
||||||
|
try {
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
appToken: wxPusherBotAppToken,
|
||||||
|
content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,
|
||||||
|
summary: this.title,
|
||||||
|
contentType: 2,
|
||||||
|
topicIds: topicIds,
|
||||||
|
uids: uids,
|
||||||
|
verifyPayType: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
if (res.code === 1000) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async chronocat() {
|
||||||
|
const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
|
||||||
|
try {
|
||||||
|
const user_ids = chronocatQQ
|
||||||
|
.match(/user_id=(\d+)/g)
|
||||||
|
?.map((match: any) => match.split('=')[1]);
|
||||||
|
const group_ids = chronocatQQ
|
||||||
|
.match(/group_id=(\d+)/g)
|
||||||
|
?.map((match: any) => match.split('=')[1]);
|
||||||
|
|
||||||
|
const url = `${chronocatURL}/api/message/send`;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${chronocatToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [chat_type, ids] of [
|
||||||
|
[1, user_ids],
|
||||||
|
[2, group_ids],
|
||||||
|
]) {
|
||||||
|
if (!ids) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ids: any = ids;
|
||||||
|
for (const chat_id of _ids) {
|
||||||
|
const data = {
|
||||||
|
peer: {
|
||||||
|
chatType: chat_type,
|
||||||
|
peerUin: chat_id,
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
elementType: 1,
|
||||||
|
textElement: {
|
||||||
|
content: `${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const res: any = await got.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: data,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async webhook() {
|
private async webhook() {
|
||||||
const { webhookUrl, webhookBody, webhookHeaders, webhookMethod, webhookContentType } =
|
const {
|
||||||
this.params;
|
webhookUrl,
|
||||||
|
webhookBody,
|
||||||
|
webhookHeaders,
|
||||||
|
webhookMethod,
|
||||||
|
webhookContentType,
|
||||||
|
} = this.params;
|
||||||
|
|
||||||
const { formatBody, formatUrl } = this.formatNotifyContent(webhookUrl, webhookBody);
|
if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) {
|
||||||
if (!formatUrl && !formatBody) {
|
throw new Error('Url 或者 Body 中必须包含 $title');
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = parseHeaders(webhookHeaders);
|
const headers = parseHeaders(webhookHeaders);
|
||||||
const body = parseBody(formatBody, webhookContentType);
|
const body = parseBody(webhookBody, webhookContentType, (v) =>
|
||||||
|
v?.replaceAll('$title', this.title)?.replaceAll('$content', this.content),
|
||||||
|
);
|
||||||
const bodyParam = this.formatBody(webhookContentType, body);
|
const bodyParam = this.formatBody(webhookContentType, body);
|
||||||
const options = {
|
const options = {
|
||||||
method: webhookMethod,
|
method: webhookMethod,
|
||||||
headers,
|
headers,
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
allowGetBody: true,
|
allowGetBody: true,
|
||||||
...bodyParam
|
...bodyParam,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const formatUrl = webhookUrl
|
||||||
|
?.replaceAll('$title', encodeURIComponent(this.title))
|
||||||
|
?.replaceAll('$content', encodeURIComponent(this.content));
|
||||||
|
const res = await got(formatUrl, options);
|
||||||
|
if (String(res.statusCode).startsWith('20')) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
}
|
}
|
||||||
const res = await got(formatUrl, options);
|
|
||||||
return String(res.statusCode).startsWith('20');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatBody(contentType: string, body: any): object {
|
private formatBody(contentType: string, body: any): object {
|
||||||
@@ -416,19 +841,9 @@ export default class NotificationService {
|
|||||||
case 'multipart/form-data':
|
case 'multipart/form-data':
|
||||||
return { form: body };
|
return { form: body };
|
||||||
case 'application/x-www-form-urlencoded':
|
case 'application/x-www-form-urlencoded':
|
||||||
|
case 'text/plain':
|
||||||
return { body };
|
return { body };
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatNotifyContent(url: string, body: string) {
|
|
||||||
if (!url.includes('$title') && !body.includes('$title')) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
formatUrl: url.replaceAll('$title', encodeURIComponent(this.title)).replaceAll('$content', encodeURIComponent(this.content)),
|
|
||||||
formatBody: body.replaceAll('$title', this.title).replaceAll('$content', this.content),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-24
@@ -1,19 +1,18 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { createRandomString } from '../config/util';
|
import { createRandomString } from '../config/util';
|
||||||
import config from '../config';
|
|
||||||
import { App, AppModel } from '../data/open';
|
import { App, AppModel } from '../data/open';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
import sequelize, { Op } from 'sequelize';
|
import sequelize, { Op } from 'sequelize';
|
||||||
|
import { shareStore } from '../shared/store';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class OpenService {
|
export default class OpenService {
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
public async findTokenByValue(token: string): Promise<App | null> {
|
public async findApps(): Promise<App[] | null> {
|
||||||
const docs = await this.find({});
|
const docs = await this.find({});
|
||||||
const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token));
|
return docs;
|
||||||
return doc[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async create(payload: App): Promise<App> {
|
public async create(payload: App): Promise<App> {
|
||||||
@@ -21,12 +20,14 @@ export default class OpenService {
|
|||||||
tab.client_id = createRandomString(12, 12);
|
tab.client_id = createRandomString(12, 12);
|
||||||
tab.client_secret = createRandomString(24, 24);
|
tab.client_secret = createRandomString(24, 24);
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
|
const apps = await this.find({});
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
return { ...doc, tokens: [] };
|
return { ...doc, tokens: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payload: App): Promise<App> {
|
public async insert(payload: App): Promise<App> {
|
||||||
const doc = await AppModel.create(payload, { returning: true });
|
const doc = await AppModel.create(payload, { returning: true });
|
||||||
return doc.get({ plain: true }) as App;
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: App): Promise<App> {
|
public async update(payload: App): Promise<App> {
|
||||||
@@ -34,26 +35,33 @@ export default class OpenService {
|
|||||||
name: payload.name,
|
name: payload.name,
|
||||||
scopes: payload.scopes,
|
scopes: payload.scopes,
|
||||||
id: payload.id,
|
id: payload.id,
|
||||||
} as any);
|
} as App);
|
||||||
return { ...newDoc, tokens: [] };
|
return { ...newDoc, tokens: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateDb(payload: App): Promise<App> {
|
private async updateDb(payload: Partial<App>): Promise<App> {
|
||||||
await AppModel.update(payload, { where: { id: payload.id } });
|
await AppModel.update(payload, { where: { id: payload.id } });
|
||||||
return await this.getDb({ id: payload.id });
|
const apps = await this.find({});
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
|
return apps?.find((x) => x.id === payload.id) as App;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<App> {
|
public async getDb(query: Record<string, any>): Promise<App> {
|
||||||
const doc: any = await AppModel.findOne({ where: query });
|
const doc = await AppModel.findOne({ where: query });
|
||||||
return doc && (doc.get({ plain: true }) as App);
|
if (!doc) {
|
||||||
|
throw new Error(`App ${JSON.stringify(query)} not found`);
|
||||||
|
}
|
||||||
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
await AppModel.destroy({ where: { id: ids } });
|
await AppModel.destroy({ where: { id: ids } });
|
||||||
|
const apps = await this.find({});
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resetSecret(id: number): Promise<App> {
|
public async resetSecret(id: number): Promise<App> {
|
||||||
const tab: any = {
|
const tab: Partial<App> = {
|
||||||
client_secret: createRandomString(24, 24),
|
client_secret: createRandomString(24, 24),
|
||||||
tokens: [],
|
tokens: [],
|
||||||
id,
|
id,
|
||||||
@@ -71,11 +79,11 @@ export default class OpenService {
|
|||||||
public async list(
|
public async list(
|
||||||
searchText: string = '',
|
searchText: string = '',
|
||||||
sort: any = {},
|
sort: any = {},
|
||||||
query: any = {},
|
query: Record<string, any> = {},
|
||||||
): Promise<App[]> {
|
): Promise<App[]> {
|
||||||
let condition = { ...query };
|
let condition = { ...query };
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const encodeText = encodeURIComponent(searchText);
|
const encodeText = encodeURI(searchText);
|
||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchText}%` },
|
{ [Op.like]: `%${searchText}%` },
|
||||||
@@ -98,7 +106,7 @@ export default class OpenService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async find(query: any, sort?: any): Promise<App[]> {
|
private async find(query: Record<string, any>, sort?: any): Promise<App[]> {
|
||||||
const docs = await AppModel.findAll({ where: { ...query } });
|
const docs = await AppModel.findAll({ where: { ...query } });
|
||||||
return docs.map((x) => x.get({ plain: true }));
|
return docs.map((x) => x.get({ plain: true }));
|
||||||
}
|
}
|
||||||
@@ -119,7 +127,7 @@ export default class OpenService {
|
|||||||
(x) => x.expiration >= timestamp,
|
(x) => x.expiration >= timestamp,
|
||||||
);
|
);
|
||||||
let tokens = invalidTokens;
|
let tokens = invalidTokens;
|
||||||
if (invalidTokens.length > 5) {
|
if (invalidTokens.length >= 5) {
|
||||||
tokens = [
|
tokens = [
|
||||||
...invalidTokens.slice(0, 4),
|
...invalidTokens.slice(0, 4),
|
||||||
{ ...invalidTokens[4], expiration },
|
{ ...invalidTokens[4], expiration },
|
||||||
@@ -132,6 +140,8 @@ export default class OpenService {
|
|||||||
{ tokens },
|
{ tokens },
|
||||||
{ where: { client_id, client_secret } },
|
{ where: { client_id, client_secret } },
|
||||||
);
|
);
|
||||||
|
const apps = await this.find({});
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data: {
|
data: {
|
||||||
@@ -141,7 +151,7 @@ export default class OpenService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return { code: 400, message: 'client_id或client_seret有误' };
|
return { code: 400, message: 'client_id 或 client_seret 有误' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,14 +159,15 @@ export default class OpenService {
|
|||||||
value: string;
|
value: string;
|
||||||
expiration: number;
|
expiration: number;
|
||||||
}> {
|
}> {
|
||||||
let systemApp = (await AppModel.findOne({
|
const apps = await shareStore.getApps();
|
||||||
where: { name: 'system' },
|
const systemApp = apps?.find((x) => x.name === 'system');
|
||||||
})) as App;
|
|
||||||
if (!systemApp) {
|
if (!systemApp) {
|
||||||
systemApp = await this.create({
|
throw new Error('system app not found');
|
||||||
name: 'system',
|
}
|
||||||
scopes: ['crons', 'system'],
|
const now = Math.round(Date.now() / 1000);
|
||||||
} as App);
|
const currentToken = systemApp.tokens?.find((x) => x.expiration > now);
|
||||||
|
if (currentToken) {
|
||||||
|
return currentToken;
|
||||||
}
|
}
|
||||||
const { data } = await this.authToken({
|
const { data } = await this.authToken({
|
||||||
client_id: systemApp.client_id,
|
client_id: systemApp.client_id,
|
||||||
|
|||||||
+132
-79
@@ -1,20 +1,23 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import nodeSchedule from 'node-schedule';
|
import nodeSchedule from 'node-schedule';
|
||||||
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
|
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||||
import {
|
import {
|
||||||
ToadScheduler,
|
ToadScheduler,
|
||||||
LongIntervalJob,
|
LongIntervalJob,
|
||||||
AsyncTask,
|
|
||||||
SimpleIntervalSchedule,
|
SimpleIntervalSchedule,
|
||||||
|
Task,
|
||||||
} from 'toad-scheduler';
|
} from 'toad-scheduler';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
import { spawn } from 'cross-spawn';
|
||||||
|
|
||||||
interface ScheduleTaskType {
|
export interface ScheduleTaskType {
|
||||||
id: number;
|
id?: number;
|
||||||
command: string;
|
command: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
schedule?: string;
|
schedule?: string;
|
||||||
|
runOrigin: 'subscription' | 'system' | 'script';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskCallbacks {
|
export interface TaskCallbacks {
|
||||||
@@ -38,81 +41,105 @@ export default class ScheduleService {
|
|||||||
|
|
||||||
private intervalSchedule = new ToadScheduler();
|
private intervalSchedule = new ToadScheduler();
|
||||||
|
|
||||||
private maxBuffer = 200 * 1024 * 1024;
|
private taskLimitMap = {
|
||||||
|
system: 'runWithSystemLimit' as const,
|
||||||
|
script: 'runWithScriptLimit' as const,
|
||||||
|
subscription: 'runWithSubscriptionLimit' as const,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
async runTask(command: string, callbacks: TaskCallbacks = {}) {
|
async runTask(
|
||||||
return new Promise(async (resolve, reject) => {
|
command: string,
|
||||||
try {
|
callbacks: TaskCallbacks = {},
|
||||||
const startTime = dayjs();
|
params: {
|
||||||
await callbacks.onBefore?.(startTime);
|
schedule?: string;
|
||||||
|
name?: string;
|
||||||
|
command?: string;
|
||||||
|
id: string;
|
||||||
|
runOrigin: 'subscription' | 'system' | 'script';
|
||||||
|
},
|
||||||
|
completionTime: 'start' | 'end' = 'end',
|
||||||
|
) {
|
||||||
|
const { runOrigin, ...others } = params;
|
||||||
|
|
||||||
const cp = spawn(command, { shell: '/bin/bash' });
|
return taskLimit[this.taskLimitMap[runOrigin]](others, () => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
// TODO:
|
this.logger.info(
|
||||||
callbacks.onStart?.(cp, startTime);
|
`[panel][开始执行任务] 参数: ${JSON.stringify({
|
||||||
|
...others,
|
||||||
cp.stdout.on('data', async (data) => {
|
|
||||||
await callbacks.onLog?.(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.stderr.on('data', async (data) => {
|
|
||||||
this.logger.error(
|
|
||||||
'执行任务 %s 失败,时间:%s, 错误信息:%j',
|
|
||||||
command,
|
command,
|
||||||
new Date().toLocaleString(),
|
})}`,
|
||||||
data.toString(),
|
|
||||||
);
|
|
||||||
await callbacks.onError?.(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('error', async (err) => {
|
|
||||||
this.logger.error(
|
|
||||||
'创建任务 %s 失败,时间:%s, 错误信息:%j',
|
|
||||||
command,
|
|
||||||
new Date().toLocaleString(),
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
await callbacks.onError?.(JSON.stringify(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('exit', async (code, signal) => {
|
|
||||||
this.logger.info(
|
|
||||||
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('close', async (code) => {
|
|
||||||
const endTime = dayjs();
|
|
||||||
await callbacks.onEnd?.(
|
|
||||||
cp,
|
|
||||||
endTime,
|
|
||||||
endTime.diff(startTime, 'seconds'),
|
|
||||||
);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await this.logger.error(
|
|
||||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
|
||||||
command,
|
|
||||||
new Date().toLocaleString(),
|
|
||||||
error,
|
|
||||||
);
|
);
|
||||||
await callbacks.onError?.(JSON.stringify(error));
|
|
||||||
resolve(null);
|
try {
|
||||||
}
|
const startTime = dayjs();
|
||||||
|
await callbacks.onBefore?.(startTime);
|
||||||
|
|
||||||
|
const cp = spawn(command, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
callbacks.onStart?.(cp, startTime);
|
||||||
|
completionTime === 'start' && resolve(cp.pid);
|
||||||
|
|
||||||
|
cp.stdout.on('data', async (data) => {
|
||||||
|
await callbacks.onLog?.(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stderr.on('data', async (data) => {
|
||||||
|
this.logger.info(
|
||||||
|
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
command,
|
||||||
|
data.toString(),
|
||||||
|
);
|
||||||
|
await callbacks.onError?.(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('error', async (err) => {
|
||||||
|
this.logger.error(
|
||||||
|
'[panel][创建任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
command,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
await callbacks.onError?.(JSON.stringify(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code) => {
|
||||||
|
this.logger.info(
|
||||||
|
'[panel][执行任务结束] 参数: %s, 退出码: %j',
|
||||||
|
JSON.stringify({
|
||||||
|
...others,
|
||||||
|
command,
|
||||||
|
}),
|
||||||
|
code,
|
||||||
|
);
|
||||||
|
const endTime = dayjs();
|
||||||
|
await callbacks.onEnd?.(
|
||||||
|
cp,
|
||||||
|
endTime,
|
||||||
|
endTime.diff(startTime, 'seconds'),
|
||||||
|
);
|
||||||
|
resolve({ ...others, pid: cp.pid, code });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
command,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
await callbacks.onError?.(JSON.stringify(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCronTask(
|
async createCronTask(
|
||||||
{ id = 0, command, name, schedule = '' }: ScheduleTaskType,
|
{ id = 0, command, name, schedule = '', runOrigin }: ScheduleTaskType,
|
||||||
callbacks?: TaskCallbacks,
|
callbacks?: TaskCallbacks,
|
||||||
runImmediately = false,
|
runImmediately = false,
|
||||||
) {
|
) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'[创建cron任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
'[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
|
||||||
_id,
|
_id,
|
||||||
schedule,
|
schedule,
|
||||||
name,
|
name,
|
||||||
@@ -122,46 +149,63 @@ export default class ScheduleService {
|
|||||||
this.scheduleStacks.set(
|
this.scheduleStacks.set(
|
||||||
_id,
|
_id,
|
||||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||||
await this.runTask(command, callbacks);
|
this.runTask(command, callbacks, {
|
||||||
|
name,
|
||||||
|
schedule,
|
||||||
|
command,
|
||||||
|
id: _id,
|
||||||
|
runOrigin,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (runImmediately) {
|
if (runImmediately) {
|
||||||
await this.runTask(command, callbacks);
|
this.runTask(command, callbacks, {
|
||||||
|
name,
|
||||||
|
schedule,
|
||||||
|
command,
|
||||||
|
id: _id,
|
||||||
|
runOrigin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info('[取消定时任务],任务名:%s', name);
|
this.logger.info('[panel][取消定时任务], 任务名: %s', name);
|
||||||
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
if (this.scheduleStacks.has(_id)) {
|
||||||
|
this.scheduleStacks.get(_id)?.cancel();
|
||||||
|
this.scheduleStacks.delete(_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createIntervalTask(
|
async createIntervalTask(
|
||||||
{ id = 0, command, name = '' }: ScheduleTaskType,
|
{ id = 0, command, name = '', runOrigin }: ScheduleTaskType,
|
||||||
schedule: SimpleIntervalSchedule,
|
schedule: SimpleIntervalSchedule,
|
||||||
runImmediately = true,
|
runImmediately = true,
|
||||||
callbacks?: TaskCallbacks,
|
callbacks?: TaskCallbacks,
|
||||||
) {
|
) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
|
'[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s',
|
||||||
_id,
|
_id,
|
||||||
name,
|
name,
|
||||||
command,
|
command,
|
||||||
);
|
);
|
||||||
const task = new AsyncTask(
|
const task = new Task(
|
||||||
name,
|
name,
|
||||||
async () => {
|
() => {
|
||||||
return new Promise(async (resolve, reject) => {
|
this.runTask(command, callbacks, {
|
||||||
await this.runTask(command, callbacks);
|
name,
|
||||||
|
command,
|
||||||
|
id: _id,
|
||||||
|
runOrigin,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
command,
|
command,
|
||||||
new Date().toLocaleString(),
|
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -170,19 +214,28 @@ export default class ScheduleService {
|
|||||||
const job = new LongIntervalJob(
|
const job = new LongIntervalJob(
|
||||||
{ runImmediately: false, ...schedule },
|
{ runImmediately: false, ...schedule },
|
||||||
task,
|
task,
|
||||||
_id,
|
{ id: _id },
|
||||||
);
|
);
|
||||||
|
|
||||||
this.intervalSchedule.addIntervalJob(job);
|
this.intervalSchedule.addIntervalJob(job);
|
||||||
|
|
||||||
if (runImmediately) {
|
if (runImmediately) {
|
||||||
await this.runTask(command, callbacks);
|
this.runTask(command, callbacks, {
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
id: _id,
|
||||||
|
runOrigin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
|
this.logger.info(
|
||||||
|
'[panel][取消interval任务], 任务ID: %s, 任务名: %s',
|
||||||
|
_id,
|
||||||
|
name,
|
||||||
|
);
|
||||||
this.intervalSchedule.removeById(_id);
|
this.intervalSchedule.removeById(_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-18
@@ -1,11 +1,13 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import fs from 'fs';
|
import path, { join } from 'path';
|
||||||
import path from 'path';
|
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import CronService from './cron';
|
import CronService from './cron';
|
||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { TASK_COMMAND } from '../config/const';
|
||||||
|
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ScriptService {
|
export default class ScriptService {
|
||||||
@@ -19,9 +21,7 @@ export default class ScriptService {
|
|||||||
private taskCallbacks(filePath: string): TaskCallbacks {
|
private taskCallbacks(filePath: string): TaskCallbacks {
|
||||||
return {
|
return {
|
||||||
onEnd: async (cp, endTime, diff) => {
|
onEnd: async (cp, endTime, diff) => {
|
||||||
try {
|
await rmPath(filePath);
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
} catch (error) {}
|
|
||||||
},
|
},
|
||||||
onError: async (message: string) => {
|
onError: async (message: string) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
@@ -40,24 +40,38 @@ export default class ScriptService {
|
|||||||
|
|
||||||
public async runScript(filePath: string) {
|
public async runScript(filePath: string) {
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
const command = `task -l ${relativePath} now`;
|
const command = `${TASK_COMMAND} ${relativePath} now`;
|
||||||
this.scheduleService.runTask(command, this.taskCallbacks(filePath));
|
const pid = await this.scheduleService.runTask(
|
||||||
|
`real_time=true ${command}`,
|
||||||
|
this.taskCallbacks(filePath),
|
||||||
|
{ command, id: relativePath.replace(/ /g, '-'), runOrigin: 'script' },
|
||||||
|
'start',
|
||||||
|
);
|
||||||
|
|
||||||
|
return { code: 200, data: pid };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stopScript(filePath: string, pid: number) {
|
||||||
|
if (!pid) {
|
||||||
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
|
taskLimit.removeQueuedCron(relativePath.replace(/ /g, '-'));
|
||||||
|
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await killTask(pid);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stopScript(filePath: string) {
|
public async getFile(filePath: string, fileName: string) {
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
const finalPath = path.resolve(config.scriptPath, filePath, fileName);
|
||||||
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
|
|
||||||
|
|
||||||
const str = err ? `\n${err}` : '';
|
if (!finalPath.startsWith(config.scriptPath)) {
|
||||||
this.sockService.sendMessage({
|
return '';
|
||||||
type: 'manuallyRunScript',
|
}
|
||||||
message: `${str}\n## 执行结束... ${new Date()
|
|
||||||
.toLocaleString('zh', { hour12: false })
|
|
||||||
.replace(' 24:', ' 00:')} `,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { code: 200 };
|
const content = await getFileContentByName(finalPath);
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { SockMessage } from '../data/sock';
|
|||||||
export default class SockService {
|
export default class SockService {
|
||||||
private clients: Connection[] = [];
|
private clients: Connection[] = [];
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||||
|
|
||||||
public getClients() {
|
public getClients() {
|
||||||
return this.clients;
|
return this.clients;
|
||||||
|
|||||||
+95
-49
@@ -1,84 +1,130 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import fs from 'fs';
|
import fs from 'fs/promises';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { Subscription } from '../data/subscription';
|
||||||
|
import { formatUrl } from '../config/subscription';
|
||||||
|
import config from '../config';
|
||||||
|
import { fileExist, rmPath } from '../config/util';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SshKeyService {
|
export default class SshKeyService {
|
||||||
private homedir = os.homedir();
|
private homedir = os.homedir();
|
||||||
private sshPath = path.resolve(this.homedir, '.ssh');
|
private sshPath = config.sshdPath;
|
||||||
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
|
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');
|
||||||
|
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {
|
||||||
|
this.initSshConfigFile();
|
||||||
|
}
|
||||||
|
|
||||||
private generatePrivateKeyFile(alias: string, key: string): void {
|
private async initSshConfigFile() {
|
||||||
|
let config = '';
|
||||||
|
const _exist = await fileExist(this.sshConfigFilePath);
|
||||||
|
if (_exist) {
|
||||||
|
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
|
||||||
|
} else {
|
||||||
|
await writeFileWithLock(this.sshConfigFilePath, '');
|
||||||
|
}
|
||||||
|
if (!config.includes(this.sshConfigHeader)) {
|
||||||
|
await writeFileWithLock(
|
||||||
|
this.sshConfigFilePath,
|
||||||
|
`${this.sshConfigHeader}\n\n${config}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generatePrivateKeyFile(
|
||||||
|
alias: string,
|
||||||
|
key: string,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
|
await writeFileWithLock(
|
||||||
encoding: 'utf8',
|
path.join(this.sshPath, alias),
|
||||||
mode: '400',
|
`${key}${os.EOL}`,
|
||||||
});
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
mode: '400',
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('生成私钥文件失败', error);
|
this.logger.error('生成私钥文件失败', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConfigRegx(alias: string) {
|
private async removePrivateKeyFile(alias: string): Promise<void> {
|
||||||
return new RegExp(
|
|
||||||
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
|
|
||||||
'g',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private removePrivateKeyFile(alias: string): void {
|
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(`${this.sshPath}/${alias}`);
|
const filePath = path.join(this.sshPath, alias);
|
||||||
|
await rmPath(filePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('删除私钥文件失败', error);
|
this.logger.error('删除私钥文件失败', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateSingleSshConfig(alias: string, host: string): string {
|
private async generateSingleSshConfig(
|
||||||
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
|
alias: string,
|
||||||
}
|
host: string,
|
||||||
|
proxy?: string,
|
||||||
private generateSshConfig(configs: string[]) {
|
) {
|
||||||
try {
|
if (host === 'github.com') {
|
||||||
for (const config of configs) {
|
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa`;
|
||||||
fs.appendFileSync(this.sshConfigFilePath, config, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('写入ssh配置文件失败', error);
|
|
||||||
}
|
}
|
||||||
|
const proxyStr = proxy
|
||||||
|
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
|
||||||
|
: '';
|
||||||
|
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
|
||||||
|
this.sshPath,
|
||||||
|
alias,
|
||||||
|
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
||||||
|
await writeFileWithLock(
|
||||||
|
`${path.join(this.sshPath, `${alias}.config`)}`,
|
||||||
|
config,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeSshConfig(alias: string) {
|
private async removeSshConfig(alias: string) {
|
||||||
try {
|
try {
|
||||||
const configRegx = this.getConfigRegx(alias);
|
const filePath = path.join(this.sshPath, `${alias}.config`);
|
||||||
const data = fs
|
await rmPath(filePath);
|
||||||
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
|
|
||||||
.replace(configRegx, '')
|
|
||||||
.replace(/\n[\n]+/g, '\n');
|
|
||||||
fs.writeFileSync(this.sshConfigFilePath, data, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`删除ssh配置文件${alias}失败`, error);
|
this.logger.error(`删除ssh配置文件${alias}失败`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addSSHKey(key: string, alias: string, host: string): void {
|
public async addSSHKey(
|
||||||
this.generatePrivateKeyFile(alias, key);
|
key: string,
|
||||||
const config = this.generateSingleSshConfig(alias, host);
|
alias: string,
|
||||||
this.removeSshConfig(alias);
|
host: string,
|
||||||
this.generateSshConfig([config]);
|
proxy?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.generatePrivateKeyFile(alias, key);
|
||||||
|
await this.generateSingleSshConfig(alias, host, proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeSSHKey(alias: string, host: string): void {
|
public async removeSSHKey(
|
||||||
this.removePrivateKeyFile(alias);
|
alias: string,
|
||||||
const config = this.generateSingleSshConfig(alias, host);
|
host: string,
|
||||||
this.removeSshConfig(config);
|
proxy?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.removePrivateKeyFile(alias);
|
||||||
|
await this.removeSshConfig(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setSshConfig(docs: Subscription[]) {
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
|
||||||
|
const { alias, proxy } = doc;
|
||||||
|
const { host } = formatUrl(doc);
|
||||||
|
await this.removePrivateKeyFile(alias);
|
||||||
|
await this.removeSshConfig(alias);
|
||||||
|
await this.generatePrivateKeyFile(
|
||||||
|
alias,
|
||||||
|
(doc.pull_option as any).private_key,
|
||||||
|
);
|
||||||
|
await this.generateSingleSshConfig(alias, host, proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-181
@@ -3,32 +3,34 @@ import winston from 'winston';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import {
|
import {
|
||||||
Subscription,
|
Subscription,
|
||||||
|
SubscriptionInstance,
|
||||||
SubscriptionModel,
|
SubscriptionModel,
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
} from '../data/subscription';
|
} from '../data/subscription';
|
||||||
import {
|
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||||
ChildProcessWithoutNullStreams,
|
|
||||||
exec,
|
|
||||||
execSync,
|
|
||||||
spawn,
|
|
||||||
} from 'child_process';
|
|
||||||
import fs from 'fs';
|
|
||||||
import cron_parser from 'cron-parser';
|
|
||||||
import {
|
import {
|
||||||
getFileContentByName,
|
getFileContentByName,
|
||||||
concurrentRun,
|
concurrentRun,
|
||||||
fileExist,
|
fileExist,
|
||||||
createFile,
|
createFile,
|
||||||
|
killTask,
|
||||||
|
handleLogPath,
|
||||||
|
promiseExec,
|
||||||
|
rmPath,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import { promises, existsSync } from 'fs';
|
import fs from 'fs/promises';
|
||||||
import { promisify } from 'util';
|
import { FindOptions, Op } from 'sequelize';
|
||||||
import { Op } from 'sequelize';
|
import path, { join } from 'path';
|
||||||
import path from 'path';
|
|
||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import SshKeyService from './sshKey';
|
import SshKeyService from './sshKey';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { LOG_END_SYMBOL } from '../config/const';
|
||||||
|
import { formatCommand, formatUrl } from '../config/subscription';
|
||||||
|
import { CrontabModel } from '../data/cron';
|
||||||
|
import CrontabService from './cron';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionService {
|
export default class SubscriptionService {
|
||||||
@@ -37,15 +39,20 @@ export default class SubscriptionService {
|
|||||||
private scheduleService: ScheduleService,
|
private scheduleService: ScheduleService,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
private sshKeyService: SshKeyService,
|
private sshKeyService: SshKeyService,
|
||||||
|
private crontabService: CrontabService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async list(searchText?: string): Promise<Subscription[]> {
|
public async list(
|
||||||
|
searchText?: string,
|
||||||
|
ids?: string,
|
||||||
|
): Promise<SubscriptionInstance[]> {
|
||||||
let query = {};
|
let query = {};
|
||||||
|
const subIds = JSON.parse(ids || '[]');
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchText}%` },
|
{ [Op.like]: `%${searchText}%` },
|
||||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
{ [Op.like]: `%${encodeURI(searchText)}%` },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
query = {
|
query = {
|
||||||
@@ -61,83 +68,41 @@ export default class SubscriptionService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await SubscriptionModel.findAll({
|
const result = await SubscriptionModel.findAll({
|
||||||
where: query,
|
where: { ...query, ...(ids ? { id: subIds } : undefined) },
|
||||||
order: [
|
order: [
|
||||||
['is_disabled', 'ASC'],
|
['is_disabled', 'ASC'],
|
||||||
['createdAt', 'DESC'],
|
['createdAt', 'DESC'],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return result as any;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatCommand(doc: Subscription, url?: string) {
|
|
||||||
let command = 'ql ';
|
|
||||||
let _url = url || this.formatUrl(doc).url;
|
|
||||||
const { type, whitelist, blacklist, dependences, branch, extensions } = doc;
|
|
||||||
if (type === 'file') {
|
|
||||||
command += `raw "${_url}"`;
|
|
||||||
} else {
|
|
||||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
|
||||||
dependences || ''
|
|
||||||
}" "${branch || ''}" "${extensions || ''}"`;
|
|
||||||
}
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatUrl(doc: Subscription) {
|
|
||||||
let url = doc.url;
|
|
||||||
let host = '';
|
|
||||||
if (doc.type === 'private-repo') {
|
|
||||||
if (doc.pull_type === 'ssh-key') {
|
|
||||||
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
|
|
||||||
url = doc.url!.replace(host, doc.alias);
|
|
||||||
} else {
|
|
||||||
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
|
|
||||||
const { username, password } = doc.pull_option as any;
|
|
||||||
url = doc.url!.replace(host, `${username}:${password}@${host}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { url, host };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleTask(
|
public async handleTask(
|
||||||
doc: Subscription,
|
doc: Subscription,
|
||||||
needCreate = true,
|
needCreate = true,
|
||||||
needAddKey = true,
|
|
||||||
runImmediately = false,
|
runImmediately = false,
|
||||||
) {
|
) {
|
||||||
const { url, host } = this.formatUrl(doc);
|
const { url } = formatUrl(doc);
|
||||||
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
|
|
||||||
if (needAddKey) {
|
|
||||||
this.sshKeyService.addSSHKey(
|
|
||||||
(doc.pull_option as any).private_key,
|
|
||||||
doc.alias,
|
|
||||||
host,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.sshKeyService.removeSSHKey(doc.alias, host);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.command = this.formatCommand(doc, url as string);
|
doc.command = formatCommand(doc, url as string);
|
||||||
|
|
||||||
if (doc.schedule_type === 'crontab') {
|
if (doc.schedule_type === 'crontab') {
|
||||||
this.scheduleService.cancelCronTask(doc as any);
|
this.scheduleService.cancelCronTask(doc as any);
|
||||||
needCreate &&
|
needCreate &&
|
||||||
(await this.scheduleService.createCronTask(
|
(await this.scheduleService.createCronTask(
|
||||||
doc as any,
|
{ ...doc, runOrigin: 'subscription' } as any,
|
||||||
this.taskCallbacks(doc),
|
this.taskCallbacks(doc),
|
||||||
runImmediately,
|
runImmediately,
|
||||||
));
|
));
|
||||||
} else {
|
} else if (doc.interval_schedule) {
|
||||||
this.scheduleService.cancelIntervalTask(doc as any);
|
this.scheduleService.cancelIntervalTask(doc as any);
|
||||||
const { type, value } = doc.interval_schedule as any;
|
const { type, value } = doc.interval_schedule;
|
||||||
needCreate &&
|
needCreate &&
|
||||||
(await this.scheduleService.createIntervalTask(
|
(await this.scheduleService.createIntervalTask(
|
||||||
doc as any,
|
{ ...doc, runOrigin: 'subscription' } as any,
|
||||||
{ [type]: value } as SimpleIntervalSchedule,
|
{ [type]: value } as SimpleIntervalSchedule,
|
||||||
runImmediately,
|
runImmediately,
|
||||||
this.taskCallbacks(doc),
|
this.taskCallbacks(doc),
|
||||||
@@ -145,28 +110,9 @@ export default class SubscriptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async promiseExec(command: string): Promise<string> {
|
public async setSshConfig() {
|
||||||
return new Promise((resolve, reject) => {
|
const docs = await SubscriptionModel.findAll();
|
||||||
exec(
|
await this.sshKeyService.setSshConfig(docs);
|
||||||
command,
|
|
||||||
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
|
|
||||||
(err, stdout, stderr) => {
|
|
||||||
resolve(stdout || stderr || JSON.stringify(err));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleLogPath(
|
|
||||||
logPath: string,
|
|
||||||
data: string = '',
|
|
||||||
): Promise<string> {
|
|
||||||
const absolutePath = path.resolve(config.logPath, logPath);
|
|
||||||
const logFileExist = await fileExist(absolutePath);
|
|
||||||
if (!logFileExist) {
|
|
||||||
await createFile(absolutePath, data);
|
|
||||||
}
|
|
||||||
return absolutePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private taskCallbacks(doc: Subscription): TaskCallbacks {
|
private taskCallbacks(doc: Subscription): TaskCallbacks {
|
||||||
@@ -181,7 +127,7 @@ export default class SubscriptionService {
|
|||||||
},
|
},
|
||||||
{ where: { id: doc.id } },
|
{ where: { id: doc.id } },
|
||||||
);
|
);
|
||||||
const absolutePath = await this.handleLogPath(
|
const absolutePath = await handleLogPath(
|
||||||
logPath as string,
|
logPath as string,
|
||||||
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
|
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
|
||||||
);
|
);
|
||||||
@@ -190,15 +136,15 @@ export default class SubscriptionService {
|
|||||||
let beforeStr = '';
|
let beforeStr = '';
|
||||||
try {
|
try {
|
||||||
if (doc.sub_before) {
|
if (doc.sub_before) {
|
||||||
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
|
await fs.appendFile(absolutePath, `\n## 执行before命令...\n\n`);
|
||||||
beforeStr = await this.promiseExec(doc.sub_before);
|
beforeStr = await promiseExec(doc.sub_before);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
beforeStr =
|
beforeStr =
|
||||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
||||||
}
|
}
|
||||||
if (beforeStr) {
|
if (beforeStr) {
|
||||||
fs.appendFileSync(absolutePath, `${beforeStr}\n`);
|
await fs.appendFile(absolutePath, `${beforeStr}\n`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
|
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
|
||||||
@@ -211,28 +157,28 @@ export default class SubscriptionService {
|
|||||||
},
|
},
|
||||||
onEnd: async (cp, endTime, diff) => {
|
onEnd: async (cp, endTime, diff) => {
|
||||||
const sub = await this.getDb({ id: doc.id });
|
const sub = await this.getDb({ id: doc.id });
|
||||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||||
|
|
||||||
// 执行 sub_after
|
// 执行 sub_after
|
||||||
let afterStr = '';
|
let afterStr = '';
|
||||||
try {
|
try {
|
||||||
if (sub.sub_after) {
|
if (sub.sub_after) {
|
||||||
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
|
await fs.appendFile(absolutePath, `\n\n## 执行after命令...\n\n`);
|
||||||
afterStr = await this.promiseExec(sub.sub_after);
|
afterStr = await promiseExec(sub.sub_after);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
afterStr =
|
afterStr =
|
||||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
||||||
}
|
}
|
||||||
if (afterStr) {
|
if (afterStr) {
|
||||||
fs.appendFileSync(absolutePath, `${afterStr}\n`);
|
await fs.appendFile(absolutePath, `${afterStr}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.appendFileSync(
|
await fs.appendFile(
|
||||||
absolutePath,
|
absolutePath,
|
||||||
`\n## 执行结束... ${endTime.format(
|
`\n## 执行结束... ${endTime.format(
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
)} 耗时 ${diff} 秒`,
|
)} 耗时 ${diff} 秒${LOG_END_SYMBOL}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await SubscriptionModel.update(
|
await SubscriptionModel.update(
|
||||||
@@ -248,13 +194,13 @@ export default class SubscriptionService {
|
|||||||
},
|
},
|
||||||
onError: async (message: string) => {
|
onError: async (message: string) => {
|
||||||
const sub = await this.getDb({ id: doc.id });
|
const sub = await this.getDb({ id: doc.id });
|
||||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
await fs.appendFile(absolutePath, `\n${message}`);
|
||||||
},
|
},
|
||||||
onLog: async (message: string) => {
|
onLog: async (message: string) => {
|
||||||
const sub = await this.getDb({ id: doc.id });
|
const sub = await this.getDb({ id: doc.id });
|
||||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
await fs.appendFile(absolutePath, `\n${message}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -262,17 +208,21 @@ export default class SubscriptionService {
|
|||||||
public async create(payload: Subscription): Promise<Subscription> {
|
public async create(payload: Subscription): Promise<Subscription> {
|
||||||
const tab = new Subscription(payload);
|
const tab = new Subscription(payload);
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
await this.handleTask(doc);
|
await this.handleTask(doc.get({ plain: true }));
|
||||||
|
await this.setSshConfig();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payload: Subscription): Promise<Subscription> {
|
public async insert(payload: Subscription): Promise<SubscriptionInstance> {
|
||||||
return await SubscriptionModel.create(payload, { returning: true });
|
return await SubscriptionModel.create(payload, { returning: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Subscription): Promise<Subscription> {
|
public async update(payload: Subscription): Promise<Subscription> {
|
||||||
const newDoc = await this.updateDb(payload);
|
const doc = await this.getDb({ id: payload.id });
|
||||||
|
const tab = new Subscription({ ...doc, ...payload });
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
await this.handleTask(newDoc, !newDoc.is_disabled);
|
await this.handleTask(newDoc, !newDoc.is_disabled);
|
||||||
|
await this.setSshConfig();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,17 +262,36 @@ export default class SubscriptionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[], query: { force?: boolean }) {
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await this.handleTask(doc, false, false);
|
await this.handleTask(doc.get({ plain: true }), false);
|
||||||
}
|
}
|
||||||
await SubscriptionModel.destroy({ where: { id: ids } });
|
await SubscriptionModel.destroy({ where: { id: ids } });
|
||||||
|
await this.setSshConfig();
|
||||||
|
|
||||||
|
if (query?.force === true) {
|
||||||
|
const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
|
||||||
|
if (crons?.length) {
|
||||||
|
await this.crontabService.remove(crons.map((x) => x.id!));
|
||||||
|
}
|
||||||
|
for (const doc of docs) {
|
||||||
|
const filePath = join(config.scriptPath, doc.alias);
|
||||||
|
const repoPath = join(config.repoPath, doc.alias);
|
||||||
|
await rmPath(filePath);
|
||||||
|
await rmPath(repoPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<Subscription> {
|
public async getDb(
|
||||||
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
|
query: FindOptions<Subscription>['where'],
|
||||||
return doc && (doc.get({ plain: true }) as Subscription);
|
): Promise<Subscription> {
|
||||||
|
const doc = await SubscriptionModel.findOne({ where: { ...query } });
|
||||||
|
if (!doc) {
|
||||||
|
throw new Error(`Subscription ${JSON.stringify(query)} not found`);
|
||||||
|
}
|
||||||
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run(ids: number[]) {
|
public async run(ids: number[]) {
|
||||||
@@ -330,10 +299,9 @@ export default class SubscriptionService {
|
|||||||
{ status: SubscriptionStatus.queued },
|
{ status: SubscriptionStatus.queued },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
concurrentRun(
|
ids.forEach((id) => {
|
||||||
ids.map((id) => async () => await this.runSingle(id)),
|
this.runSingle(id);
|
||||||
10,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(ids: number[]) {
|
public async stop(ids: number[]) {
|
||||||
@@ -341,20 +309,11 @@ export default class SubscriptionService {
|
|||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
if (doc.pid) {
|
if (doc.pid) {
|
||||||
try {
|
try {
|
||||||
process.kill(-doc.pid);
|
await killTask(doc.pid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.silly(error);
|
this.logger.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const command = this.formatCommand(doc);
|
|
||||||
const err = await this.killTask(command);
|
|
||||||
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
|
||||||
const str = err ? `\n${err}` : '';
|
|
||||||
|
|
||||||
fs.appendFileSync(
|
|
||||||
`${absolutePath}`,
|
|
||||||
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await SubscriptionModel.update(
|
await SubscriptionModel.update(
|
||||||
@@ -363,69 +322,39 @@ export default class SubscriptionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async killTask(name: string) {
|
|
||||||
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
try {
|
|
||||||
let pid = (await execAsync(taskCommand)).stdout;
|
|
||||||
if (pid) {
|
|
||||||
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let pids = pid.match(/\(\d+/g);
|
|
||||||
const killLogs = [];
|
|
||||||
if (pids && pids.length > 0) {
|
|
||||||
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
|
|
||||||
for (const id of pids) {
|
|
||||||
const c = `kill -9 ${id.slice(1)}`;
|
|
||||||
try {
|
|
||||||
const { stdout, stderr } = await execAsync(c);
|
|
||||||
if (stderr) {
|
|
||||||
killLogs.push(stderr);
|
|
||||||
}
|
|
||||||
if (stdout) {
|
|
||||||
killLogs.push(stdout);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
killLogs.push(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
|
||||||
} catch (e) {
|
|
||||||
return JSON.stringify(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runSingle(subscriptionId: number) {
|
private async runSingle(subscriptionId: number) {
|
||||||
const subscription = await this.getDb({ id: subscriptionId });
|
const subscription = await this.getDb({ id: subscriptionId });
|
||||||
if (subscription.status !== SubscriptionStatus.queued) {
|
if (subscription.status !== SubscriptionStatus.queued) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = this.formatCommand(subscription);
|
const command = formatCommand(subscription);
|
||||||
|
|
||||||
await this.scheduleService.runTask(
|
this.scheduleService.runTask(command, this.taskCallbacks(subscription), {
|
||||||
|
name: subscription.name,
|
||||||
|
schedule: subscription.schedule,
|
||||||
command,
|
command,
|
||||||
this.taskCallbacks(subscription),
|
id: String(subscription.id),
|
||||||
);
|
runOrigin: 'subscription',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
public async disabled(ids: number[]) {
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
|
||||||
for (const doc of docs) {
|
|
||||||
await this.handleTask(doc, false);
|
|
||||||
}
|
|
||||||
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
||||||
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
await this.setSshConfig();
|
||||||
|
for (const doc of docs) {
|
||||||
|
await this.handleTask(doc.get({ plain: true }), false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
public async enabled(ids: number[]) {
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
|
||||||
for (const doc of docs) {
|
|
||||||
await this.handleTask(doc);
|
|
||||||
}
|
|
||||||
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
||||||
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
await this.setSshConfig();
|
||||||
|
for (const doc of docs) {
|
||||||
|
await this.handleTask(doc.get({ plain: true }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(id: number) {
|
public async log(id: number) {
|
||||||
@@ -434,8 +363,8 @@ export default class SubscriptionService {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
const absolutePath = await handleLogPath(doc.log_path as string);
|
||||||
return getFileContentByName(absolutePath);
|
return await getFileContentByName(absolutePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logs(id: number) {
|
public async logs(id: number) {
|
||||||
@@ -447,15 +376,18 @@ export default class SubscriptionService {
|
|||||||
if (doc.log_path) {
|
if (doc.log_path) {
|
||||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||||
const dir = path.resolve(config.logPath, relativeDir);
|
const dir = path.resolve(config.logPath, relativeDir);
|
||||||
if (existsSync(dir)) {
|
const _exist = await fileExist(dir);
|
||||||
let files = await promises.readdir(dir);
|
if (_exist) {
|
||||||
return files
|
let files = await fs.readdir(dir);
|
||||||
.map((x) => ({
|
return (
|
||||||
filename: x,
|
await Promise.all(
|
||||||
directory: relativeDir.replace(config.logPath, ''),
|
files.map(async (x) => ({
|
||||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
filename: x,
|
||||||
}))
|
directory: relativeDir.replace(config.logPath, ''),
|
||||||
.sort((a, b) => b.time - a.time);
|
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
).sort((a, b) => b.time - a.time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+352
-54
@@ -1,15 +1,41 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { spawn } from 'cross-spawn';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import fs from 'fs';
|
||||||
|
import got from 'got';
|
||||||
|
import sum from 'lodash/sum';
|
||||||
|
import path from 'path';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import { TASK_COMMAND } from '../config/const';
|
||||||
import _ from 'lodash';
|
import {
|
||||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
getPid,
|
||||||
|
killTask,
|
||||||
|
parseContentVersion,
|
||||||
|
parseVersion,
|
||||||
|
promiseExec,
|
||||||
|
readDirs,
|
||||||
|
rmPath,
|
||||||
|
} from '../config/util';
|
||||||
|
import {
|
||||||
|
DependenceModel,
|
||||||
|
DependenceStatus,
|
||||||
|
DependenceTypes,
|
||||||
|
} from '../data/dependence';
|
||||||
import { NotificationInfo } from '../data/notify';
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import {
|
||||||
|
AuthDataType,
|
||||||
|
SystemInfo,
|
||||||
|
SystemInstance,
|
||||||
|
SystemModel,
|
||||||
|
SystemModelInfo,
|
||||||
|
} from '../data/system';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
import NotificationService from './notify';
|
import NotificationService from './notify';
|
||||||
import ScheduleService from './schedule';
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import got from 'got';
|
import os from 'os';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SystemService {
|
export default class SystemService {
|
||||||
@@ -22,20 +48,24 @@ export default class SystemService {
|
|||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getLogRemoveFrequency() {
|
public async getSystemConfig() {
|
||||||
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
|
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||||
return doc || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
|
||||||
await AuthModel.upsert({ ...payload });
|
|
||||||
const doc = await this.getDb({ type: payload.type });
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<any> {
|
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
|
||||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
const { id, ...others } = payload;
|
||||||
return doc && (doc.get({ plain: true }) as any);
|
await SystemModel.update(others, { where: { id } });
|
||||||
|
const doc = await this.getDb({ id });
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(query: any): Promise<SystemInfo> {
|
||||||
|
const doc = await SystemModel.findOne({ where: query });
|
||||||
|
if (!doc) {
|
||||||
|
throw new Error(`System ${JSON.stringify(query)} not found`);
|
||||||
|
}
|
||||||
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||||
@@ -56,55 +86,215 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateLogRemoveFrequency(frequency: number) {
|
public async updateLogRemoveFrequency(info: SystemModelInfo) {
|
||||||
const oDoc = await this.getLogRemoveFrequency();
|
const oDoc = await this.getSystemConfig();
|
||||||
const result = await this.updateAuthDb({
|
const result = await this.updateAuthDb({
|
||||||
...oDoc,
|
...oDoc,
|
||||||
type: AuthDataType.removeLogFrequency,
|
info: { ...oDoc.info, ...info },
|
||||||
info: { frequency },
|
|
||||||
});
|
});
|
||||||
const cron = {
|
const cron = {
|
||||||
id: result.id,
|
id: result.id as number,
|
||||||
name: '删除日志',
|
name: '删除日志',
|
||||||
command: `ql rmlog ${frequency}`,
|
command: `ql rmlog ${info.logRemoveFrequency}`,
|
||||||
|
runOrigin: 'system' as const,
|
||||||
};
|
};
|
||||||
await this.scheduleService.cancelIntervalTask(cron);
|
if (oDoc.info?.logRemoveFrequency) {
|
||||||
if (frequency > 0) {
|
await this.scheduleService.cancelIntervalTask(cron);
|
||||||
this.scheduleService.createIntervalTask(cron, {
|
|
||||||
days: frequency,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return { code: 200, data: { ...cron } };
|
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {
|
||||||
|
this.scheduleService.createIntervalTask(
|
||||||
|
cron,
|
||||||
|
{
|
||||||
|
days: info.logRemoveFrequency,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { code: 200, data: info };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateCronConcurrency(info: SystemModelInfo) {
|
||||||
|
const oDoc = await this.getSystemConfig();
|
||||||
|
await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
info: { ...oDoc.info, ...info },
|
||||||
|
});
|
||||||
|
if (info.cronConcurrency) {
|
||||||
|
await taskLimit.setCustomLimit(info.cronConcurrency);
|
||||||
|
}
|
||||||
|
return { code: 200, data: info };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateDependenceProxy(info: SystemModelInfo) {
|
||||||
|
const oDoc = await this.getSystemConfig();
|
||||||
|
await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
info: { ...oDoc.info, ...info },
|
||||||
|
});
|
||||||
|
if (info.dependenceProxy) {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
config.dependenceProxyFile,
|
||||||
|
`export http_proxy="${info.dependenceProxy}"\nexport https_proxy="${info.dependenceProxy}"`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await fs.promises.rm(config.dependenceProxyFile);
|
||||||
|
}
|
||||||
|
return { code: 200, data: info };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateNodeMirror(info: SystemModelInfo, res?: Response) {
|
||||||
|
const oDoc = await this.getSystemConfig();
|
||||||
|
await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
info: { ...oDoc.info, ...info },
|
||||||
|
});
|
||||||
|
let cmd = 'pnpm config delete registry';
|
||||||
|
if (info.nodeMirror) {
|
||||||
|
cmd = `pnpm config set registry ${info.nodeMirror}`;
|
||||||
|
}
|
||||||
|
let command = `cd && ${cmd}`;
|
||||||
|
const docs = await DependenceModel.findAll({
|
||||||
|
where: {
|
||||||
|
type: DependenceTypes.nodejs,
|
||||||
|
status: DependenceStatus.installed,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (docs.length > 0) {
|
||||||
|
command += ` && pnpm i -g`;
|
||||||
|
}
|
||||||
|
this.scheduleService.runTask(
|
||||||
|
command,
|
||||||
|
{
|
||||||
|
onStart: async (cp) => {
|
||||||
|
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
|
||||||
|
res?.end();
|
||||||
|
},
|
||||||
|
onEnd: async () => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'updateNodeMirror',
|
||||||
|
message: 'update node mirror end',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: async (message: string) => {
|
||||||
|
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
|
||||||
|
},
|
||||||
|
onLog: async (message: string) => {
|
||||||
|
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command,
|
||||||
|
id: 'update-node-mirror',
|
||||||
|
runOrigin: 'system',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updatePythonMirror(info: SystemModelInfo) {
|
||||||
|
const oDoc = await this.getSystemConfig();
|
||||||
|
await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
info: { ...oDoc.info, ...info },
|
||||||
|
});
|
||||||
|
let cmd = 'pip config unset global.index-url';
|
||||||
|
if (info.pythonMirror) {
|
||||||
|
cmd = `pip3 config set global.index-url ${info.pythonMirror}`;
|
||||||
|
}
|
||||||
|
await promiseExec(cmd);
|
||||||
|
return { code: 200, data: info };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateLinuxMirror(
|
||||||
|
info: SystemModelInfo,
|
||||||
|
res?: Response,
|
||||||
|
onEnd?: () => void,
|
||||||
|
) {
|
||||||
|
const oDoc = await this.getSystemConfig();
|
||||||
|
await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
info: { ...oDoc.info, ...info },
|
||||||
|
});
|
||||||
|
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
|
||||||
|
let targetDomain = 'https://dl-cdn.alpinelinux.org';
|
||||||
|
if (os.platform() !== 'linux') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = await fs.promises.readFile('/etc/apk/repositories', {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
|
||||||
|
if (domainMatch) {
|
||||||
|
defaultDomain = domainMatch[1];
|
||||||
|
}
|
||||||
|
if (info.linuxMirror) {
|
||||||
|
targetDomain = info.linuxMirror;
|
||||||
|
}
|
||||||
|
const command = `sed -i 's/${defaultDomain.replace(
|
||||||
|
/\//g,
|
||||||
|
'\\/',
|
||||||
|
)}/${targetDomain.replace(
|
||||||
|
/\//g,
|
||||||
|
'\\/',
|
||||||
|
)}/g' /etc/apk/repositories && apk update -f`;
|
||||||
|
|
||||||
|
this.scheduleService.runTask(
|
||||||
|
command,
|
||||||
|
{
|
||||||
|
onStart: async (cp) => {
|
||||||
|
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
|
||||||
|
res?.end();
|
||||||
|
},
|
||||||
|
onEnd: async () => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'updateLinuxMirror',
|
||||||
|
message: 'update linux mirror end',
|
||||||
|
});
|
||||||
|
onEnd?.();
|
||||||
|
},
|
||||||
|
onError: async (message: string) => {
|
||||||
|
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
|
||||||
|
},
|
||||||
|
onLog: async (message: string) => {
|
||||||
|
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command,
|
||||||
|
id: 'update-linux-mirror',
|
||||||
|
runOrigin: 'system',
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkUpdate() {
|
public async checkUpdate() {
|
||||||
try {
|
try {
|
||||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
const currentVersionContent = await parseVersion(config.versionFile);
|
||||||
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
|
||||||
|
|
||||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
let lastVersionContent;
|
||||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
|
||||||
|
|
||||||
let lastVersion = '';
|
|
||||||
let lastLog = '';
|
|
||||||
try {
|
try {
|
||||||
const result = await got.get(config.lastVersionFile, {
|
const result = await got.get(
|
||||||
timeout: 6000,
|
`${config.lastVersionFile}?t=${Date.now()}`,
|
||||||
retry: 0,
|
{
|
||||||
});
|
timeout: 30000,
|
||||||
const lastVersionFileContent = result.body;
|
},
|
||||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
);
|
||||||
lastLog = lastVersionFileContent.match(logRegx)
|
lastVersionContent = parseContentVersion(result.body);
|
||||||
? lastVersionFileContent.match(logRegx)![1]
|
|
||||||
: '';
|
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
|
if (!lastVersionContent) {
|
||||||
|
lastVersionContent = currentVersionContent;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data: {
|
data: {
|
||||||
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
hasNewVersion: this.checkHasNewVersion(
|
||||||
lastVersion,
|
currentVersionContent.version,
|
||||||
lastLog,
|
lastVersionContent.version,
|
||||||
|
),
|
||||||
|
lastVersion: lastVersionContent.version,
|
||||||
|
lastLog: lastVersionContent.changeLog,
|
||||||
|
lastLogLink: lastVersionContent.changeLogLink,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -135,12 +325,8 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async updateSystem() {
|
public async updateSystem() {
|
||||||
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
const cp = spawn('real_time=true ql update false', { shell: '/bin/bash' });
|
||||||
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'updateSystemVersion',
|
|
||||||
message: `开始更新系统`,
|
|
||||||
});
|
|
||||||
cp.stdout.on('data', (data) => {
|
cp.stdout.on('data', (data) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'updateSystemVersion',
|
type: 'updateSystemVersion',
|
||||||
@@ -165,6 +351,13 @@ export default class SystemService {
|
|||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async reloadSystem(target?: 'system' | 'data') {
|
||||||
|
const cmd = `real_time=true ql reload ${target || ''}`;
|
||||||
|
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||||
|
cp.unref();
|
||||||
|
return { code: 200 };
|
||||||
|
}
|
||||||
|
|
||||||
public async notify({ title, content }: { title: string; content: string }) {
|
public async notify({ title, content }: { title: string; content: string }) {
|
||||||
const isSuccess = await this.notificationService.notify(title, content);
|
const isSuccess = await this.notificationService.notify(title, content);
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
@@ -173,4 +366,109 @@ export default class SystemService {
|
|||||||
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
|
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async run({ command }: { command: string }, callback: TaskCallbacks) {
|
||||||
|
if (!command.startsWith(TASK_COMMAND)) {
|
||||||
|
command = `${TASK_COMMAND} ${command}`;
|
||||||
|
}
|
||||||
|
this.scheduleService.runTask(`real_time=true ${command}`, callback, {
|
||||||
|
command,
|
||||||
|
id: command.replace(/ /g, '-'),
|
||||||
|
runOrigin: 'system',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop({ command, pid }: { command: string; pid: number }) {
|
||||||
|
if (!pid && !command) {
|
||||||
|
return { code: 400, message: '参数错误' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid) {
|
||||||
|
await killTask(pid);
|
||||||
|
return { code: 200 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!command.startsWith(TASK_COMMAND)) {
|
||||||
|
command = `${TASK_COMMAND} ${command}`;
|
||||||
|
}
|
||||||
|
const _pid = await getPid(command);
|
||||||
|
if (_pid) {
|
||||||
|
await killTask(_pid);
|
||||||
|
return { code: 200 };
|
||||||
|
} else {
|
||||||
|
return { code: 400, message: '任务未找到' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async exportData(res: Response) {
|
||||||
|
try {
|
||||||
|
await promiseExec(
|
||||||
|
`cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile} data/`,
|
||||||
|
);
|
||||||
|
res.download(config.dataTgzFile);
|
||||||
|
} catch (error: any) {
|
||||||
|
return res.send({ code: 400, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async importData() {
|
||||||
|
try {
|
||||||
|
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
|
||||||
|
const res = await promiseExec(
|
||||||
|
`cd ${config.tmpPath} && tar -zxvf ${config.dataTgzFile}`,
|
||||||
|
);
|
||||||
|
return { code: 200, data: res };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { code: 400, message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSystemLog(
|
||||||
|
res: Response,
|
||||||
|
query: {
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const startTime = dayjs(query.startTime || undefined)
|
||||||
|
.startOf('d')
|
||||||
|
.valueOf();
|
||||||
|
const endTime = dayjs(query.endTime || undefined)
|
||||||
|
.endOf('d')
|
||||||
|
.valueOf();
|
||||||
|
const result = await readDirs(config.systemLogPath, config.systemLogPath);
|
||||||
|
const logs = result
|
||||||
|
.reverse()
|
||||||
|
.filter((x) => x.title.endsWith('.log'))
|
||||||
|
.filter((x) => x.createTime >= startTime && x.createTime <= endTime);
|
||||||
|
|
||||||
|
res.set({
|
||||||
|
'Content-Length': sum(logs.map((x) => x.size)),
|
||||||
|
});
|
||||||
|
(function sendFiles(res, fileNames) {
|
||||||
|
if (fileNames.length === 0) {
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLog = fileNames.shift();
|
||||||
|
if (currentLog) {
|
||||||
|
const currentFileStream = fs.createReadStream(
|
||||||
|
path.join(config.systemLogPath, currentLog.title),
|
||||||
|
);
|
||||||
|
currentFileStream.on('end', () => {
|
||||||
|
sendFiles(res, fileNames);
|
||||||
|
});
|
||||||
|
currentFileStream.pipe(res, { end: false });
|
||||||
|
}
|
||||||
|
})(res, logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteSystemLog() {
|
||||||
|
const result = await readDirs(config.systemLogPath, config.systemLogPath);
|
||||||
|
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
|
||||||
|
for (const log of logs) {
|
||||||
|
await rmPath(path.join(config.systemLogPath, log.title));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+190
-190
@@ -1,19 +1,27 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
import { createRandomString, getNetIp } from '../config/util';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { authenticator } from '@otplib/preset-default';
|
import { authenticator } from '@otplib/preset-default';
|
||||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
import {
|
||||||
|
AuthDataType,
|
||||||
|
SystemInfo,
|
||||||
|
SystemModel,
|
||||||
|
SystemModelInfo,
|
||||||
|
LoginStatus,
|
||||||
|
AuthInfo,
|
||||||
|
} from '../data/system';
|
||||||
import { NotificationInfo } from '../data/notify';
|
import { NotificationInfo } from '../data/notify';
|
||||||
import NotificationService from './notify';
|
import NotificationService from './notify';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import ScheduleService from './schedule';
|
import ScheduleService from './schedule';
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import IP2Region from 'ip2region';
|
||||||
|
import requestIp from 'request-ip';
|
||||||
|
import uniq from 'lodash/uniq';
|
||||||
|
import { shareStore } from '../shared/store';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class UserService {
|
export default class UserService {
|
||||||
@@ -24,7 +32,7 @@ export default class UserService {
|
|||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
private scheduleService: ScheduleService,
|
private scheduleService: ScheduleService,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
public async login(
|
public async login(
|
||||||
payloads: {
|
payloads: {
|
||||||
@@ -34,156 +42,155 @@ export default class UserService {
|
|||||||
req: Request,
|
req: Request,
|
||||||
needTwoFactor = true,
|
needTwoFactor = true,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (!fs.existsSync(config.authConfigFile)) {
|
let { username, password } = payloads;
|
||||||
return this.initAuthInfo();
|
const content = await this.getAuthInfo();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
let {
|
||||||
|
username: cUsername,
|
||||||
|
password: cPassword,
|
||||||
|
retries = 0,
|
||||||
|
lastlogon,
|
||||||
|
lastip,
|
||||||
|
lastaddr,
|
||||||
|
twoFactorActivated,
|
||||||
|
tokens = {},
|
||||||
|
platform,
|
||||||
|
} = content;
|
||||||
|
const retriesTime = Math.pow(3, retries) * 1000;
|
||||||
|
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
||||||
|
const waitTime = Math.ceil(
|
||||||
|
(retriesTime - (timestamp - lastlogon)) / 1000,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
code: 410,
|
||||||
|
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||||
|
data: waitTime,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let { username, password } = payloads;
|
if (
|
||||||
const content = this.getAuthInfo();
|
username === cUsername &&
|
||||||
const timestamp = Date.now();
|
password === cPassword &&
|
||||||
if (content) {
|
twoFactorActivated &&
|
||||||
let {
|
needTwoFactor
|
||||||
username: cUsername,
|
) {
|
||||||
password: cPassword,
|
await this.updateAuthInfo(content, {
|
||||||
retries = 0,
|
isTwoFactorChecking: true,
|
||||||
lastlogon,
|
});
|
||||||
lastip,
|
return {
|
||||||
lastaddr,
|
code: 420,
|
||||||
twoFactorActivated,
|
message: '',
|
||||||
twoFactorActived,
|
};
|
||||||
tokens = {},
|
}
|
||||||
platform,
|
|
||||||
} = content;
|
|
||||||
// patch old field
|
|
||||||
twoFactorActivated = twoFactorActivated || twoFactorActived;
|
|
||||||
|
|
||||||
if (
|
const ip = requestIp.getClientIp(req) || '';
|
||||||
(cUsername === 'admin' && cPassword === 'admin') ||
|
const query = new IP2Region();
|
||||||
!cUsername ||
|
const ipAddress = query.search(ip);
|
||||||
!cPassword
|
let address = '';
|
||||||
) {
|
if (ipAddress) {
|
||||||
return this.initAuthInfo();
|
const { country, province, city, isp } = ipAddress;
|
||||||
}
|
address = uniq([country, province, city, isp]).filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
if (username === cUsername && password === cPassword) {
|
||||||
|
const data = createRandomString(50, 100);
|
||||||
|
const expiration = twoFactorActivated ? 60 : 20;
|
||||||
|
let token = jwt.sign({ data }, config.secret as any, {
|
||||||
|
expiresIn: 60 * 60 * 24 * expiration,
|
||||||
|
algorithm: 'HS384',
|
||||||
|
});
|
||||||
|
|
||||||
const retriesTime = Math.pow(3, retries) * 1000;
|
await this.updateAuthInfo(content, {
|
||||||
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
token,
|
||||||
const waitTime = Math.ceil((retriesTime - (timestamp - lastlogon)) / 1000);
|
tokens: {
|
||||||
|
...tokens,
|
||||||
|
[req.platform]: token,
|
||||||
|
},
|
||||||
|
lastlogon: timestamp,
|
||||||
|
retries: 0,
|
||||||
|
lastip: ip,
|
||||||
|
lastaddr: address,
|
||||||
|
platform: req.platform,
|
||||||
|
isTwoFactorChecking: false,
|
||||||
|
});
|
||||||
|
this.notificationService.notify(
|
||||||
|
'登录通知',
|
||||||
|
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||||
|
req.platform
|
||||||
|
}端 登录成功,ip地址 ${ip}`,
|
||||||
|
);
|
||||||
|
await this.insertDb({
|
||||||
|
type: AuthDataType.loginLog,
|
||||||
|
info: {
|
||||||
|
timestamp,
|
||||||
|
address,
|
||||||
|
ip,
|
||||||
|
platform: req.platform,
|
||||||
|
status: LoginStatus.success,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.getLoginLog();
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
await this.updateAuthInfo(content, {
|
||||||
|
retries: retries + 1,
|
||||||
|
lastlogon: timestamp,
|
||||||
|
lastip: ip,
|
||||||
|
lastaddr: address,
|
||||||
|
platform: req.platform,
|
||||||
|
});
|
||||||
|
this.notificationService.notify(
|
||||||
|
'登录通知',
|
||||||
|
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||||
|
req.platform
|
||||||
|
}端 登录失败,ip地址 ${ip}`,
|
||||||
|
);
|
||||||
|
await this.insertDb({
|
||||||
|
type: AuthDataType.loginLog,
|
||||||
|
info: {
|
||||||
|
timestamp,
|
||||||
|
address,
|
||||||
|
ip,
|
||||||
|
platform: req.platform,
|
||||||
|
status: LoginStatus.fail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.getLoginLog();
|
||||||
|
if (retries > 2) {
|
||||||
|
const waitTime = Math.round(Math.pow(3, retries + 1));
|
||||||
return {
|
return {
|
||||||
code: 410,
|
code: 410,
|
||||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||||
data: waitTime,
|
data: waitTime,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const { ip, address } = await getNetIp(req);
|
|
||||||
if (username === cUsername && password === cPassword) {
|
|
||||||
if (twoFactorActivated && needTwoFactor) {
|
|
||||||
this.updateAuthInfo(content, {
|
|
||||||
isTwoFactorChecking: true,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
code: 420,
|
|
||||||
message: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = createRandomString(50, 100);
|
|
||||||
const expiration = twoFactorActivated ? 60 : 20;
|
|
||||||
let token = jwt.sign({ data }, config.secret as any, {
|
|
||||||
expiresIn: 60 * 60 * 24 * expiration,
|
|
||||||
algorithm: 'HS384',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateAuthInfo(content, {
|
|
||||||
token,
|
|
||||||
tokens: {
|
|
||||||
...tokens,
|
|
||||||
[req.platform]: token,
|
|
||||||
},
|
|
||||||
lastlogon: timestamp,
|
|
||||||
retries: 0,
|
|
||||||
lastip: ip,
|
|
||||||
lastaddr: address,
|
|
||||||
platform: req.platform,
|
|
||||||
isTwoFactorChecking: false,
|
|
||||||
});
|
|
||||||
await this.notificationService.notify(
|
|
||||||
'登录通知',
|
|
||||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${req.platform
|
|
||||||
}端 登录成功,ip地址 ${ip}`,
|
|
||||||
);
|
|
||||||
await this.getLoginLog();
|
|
||||||
await this.insertDb({
|
|
||||||
type: AuthDataType.loginLog,
|
|
||||||
info: {
|
|
||||||
timestamp,
|
|
||||||
address,
|
|
||||||
ip,
|
|
||||||
platform: req.platform,
|
|
||||||
status: LoginStatus.success,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
this.updateAuthInfo(content, {
|
return { code: 400, message: config.authError };
|
||||||
retries: retries + 1,
|
|
||||||
lastlogon: timestamp,
|
|
||||||
lastip: ip,
|
|
||||||
lastaddr: address,
|
|
||||||
platform: req.platform,
|
|
||||||
});
|
|
||||||
await this.notificationService.notify(
|
|
||||||
'登录通知',
|
|
||||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${req.platform
|
|
||||||
}端 登录失败,ip地址 ${ip}`,
|
|
||||||
);
|
|
||||||
await this.getLoginLog();
|
|
||||||
await this.insertDb({
|
|
||||||
type: AuthDataType.loginLog,
|
|
||||||
info: {
|
|
||||||
timestamp,
|
|
||||||
address,
|
|
||||||
ip,
|
|
||||||
platform: req.platform,
|
|
||||||
status: LoginStatus.fail,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (retries > 2) {
|
|
||||||
const waitTime = Math.round(Math.pow(3, retries + 1));
|
|
||||||
return {
|
|
||||||
code: 410,
|
|
||||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
|
||||||
data: waitTime,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return { code: 400, message: config.authError };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return this.initAuthInfo();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout(platform: string): Promise<any> {
|
public async logout(platform: string): Promise<any> {
|
||||||
const authInfo = this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
this.updateAuthInfo(authInfo, {
|
await this.updateAuthInfo(authInfo, {
|
||||||
token: '',
|
token: '',
|
||||||
tokens: { ...authInfo.tokens, [platform]: '' },
|
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLoginLog(): Promise<AuthInfo[]> {
|
public async getLoginLog(): Promise<Array<SystemModelInfo | undefined>> {
|
||||||
const docs = await AuthModel.findAll({
|
const docs = await SystemModel.findAll({
|
||||||
where: { type: AuthDataType.loginLog },
|
where: { type: AuthDataType.loginLog },
|
||||||
});
|
});
|
||||||
if (docs && docs.length > 0) {
|
if (docs && docs.length > 0) {
|
||||||
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
|
const result = docs.sort(
|
||||||
|
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
|
||||||
|
);
|
||||||
if (result.length > 100) {
|
if (result.length > 100) {
|
||||||
await AuthModel.destroy({
|
const ids = result.slice(100).map((x) => x.id!);
|
||||||
where: { id: result[result.length - 1].id },
|
await SystemModel.destroy({
|
||||||
|
where: { id: ids },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result.map((x) => x.info);
|
return result.map((x) => x.info);
|
||||||
@@ -191,26 +198,11 @@ export default class UserService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
|
private async insertDb(payload: SystemInfo): Promise<SystemInfo> {
|
||||||
const doc = await AuthModel.create({ ...payload }, { returning: true });
|
const doc = await SystemModel.create({ ...payload }, { returning: true });
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initAuthInfo() {
|
|
||||||
const newPassword = createRandomString(16, 22);
|
|
||||||
fs.writeFileSync(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: 'admin',
|
|
||||||
password: newPassword,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
code: 100,
|
|
||||||
message: '已初始化密码,请前往auth.json查看并重新登录',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateUsernameAndPassword({
|
public async updateUsernameAndPassword({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
@@ -221,42 +213,33 @@ export default class UserService {
|
|||||||
if (password === 'admin') {
|
if (password === 'admin') {
|
||||||
return { code: 400, message: '密码不能设置为admin' };
|
return { code: 400, message: '密码不能设置为admin' };
|
||||||
}
|
}
|
||||||
const authInfo = this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
this.updateAuthInfo(authInfo, { username, password });
|
await this.updateAuthInfo(authInfo, { username, password });
|
||||||
return { code: 200, message: '更新成功' };
|
return { code: 200, message: '更新成功' };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAvatar(avatar: string) {
|
public async updateAvatar(avatar: string) {
|
||||||
const authInfo = this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
this.updateAuthInfo(authInfo, { avatar });
|
await this.updateAuthInfo(authInfo, { avatar });
|
||||||
return { code: 200, data: avatar, message: '更新成功' };
|
return { code: 200, data: avatar, message: '更新成功' };
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserInfo(): Promise<any> {
|
public async initTwoFactor() {
|
||||||
return new Promise((resolve) => {
|
|
||||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
|
||||||
if (err) console.log(err);
|
|
||||||
resolve(JSON.parse(data));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public initTwoFactor() {
|
|
||||||
const secret = authenticator.generateSecret();
|
const secret = authenticator.generateSecret();
|
||||||
const authInfo = this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
||||||
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
await this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||||
return { secret, url: otpauth };
|
return { secret, url: otpauth };
|
||||||
}
|
}
|
||||||
|
|
||||||
public activeTwoFactor(code: string) {
|
public async activeTwoFactor(code: string) {
|
||||||
const authInfo = this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
const isValid = authenticator.verify({
|
const isValid = authenticator.verify({
|
||||||
token: code,
|
token: code,
|
||||||
secret: authInfo.twoFactorSecret,
|
secret: authInfo.twoFactorSecret,
|
||||||
});
|
});
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
await this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
||||||
}
|
}
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
@@ -269,7 +252,7 @@ export default class UserService {
|
|||||||
}: { username: string; password: string; code: string },
|
}: { username: string; password: string; code: string },
|
||||||
req: any,
|
req: any,
|
||||||
) {
|
) {
|
||||||
const authInfo = this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
|
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
|
||||||
if (!isTwoFactorChecking) {
|
if (!isTwoFactorChecking) {
|
||||||
return { code: 450, message: '未知错误' };
|
return { code: 450, message: '未知错误' };
|
||||||
@@ -282,7 +265,7 @@ export default class UserService {
|
|||||||
return this.login({ username, password }, req, false);
|
return this.login({ username, password }, req, false);
|
||||||
} else {
|
} else {
|
||||||
const { ip, address } = await getNetIp(req);
|
const { ip, address } = await getNetIp(req);
|
||||||
this.updateAuthInfo(authInfo, {
|
await this.updateAuthInfo(authInfo, {
|
||||||
lastip: ip,
|
lastip: ip,
|
||||||
lastaddr: address,
|
lastaddr: address,
|
||||||
platform: req.platform,
|
platform: req.platform,
|
||||||
@@ -291,50 +274,58 @@ export default class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public deactiveTwoFactor() {
|
public async deactiveTwoFactor() {
|
||||||
const authInfo = this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
this.updateAuthInfo(authInfo, {
|
await this.updateAuthInfo(authInfo, {
|
||||||
twoFactorActivated: false,
|
twoFactorActivated: false,
|
||||||
twoFactorActived: false,
|
|
||||||
twoFactorSecret: '',
|
twoFactorSecret: '',
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAuthInfo() {
|
public async getAuthInfo() {
|
||||||
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
const authInfo = await shareStore.getAuthInfo();
|
||||||
return JSON.parse(content || '{}');
|
if (authInfo) {
|
||||||
|
return authInfo;
|
||||||
|
}
|
||||||
|
const doc = await this.getDb({ type: AuthDataType.authConfig });
|
||||||
|
return (doc.info || {}) as AuthInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateAuthInfo(authInfo: any, info: any) {
|
private async updateAuthInfo(authInfo: AuthInfo, info: Partial<AuthInfo>) {
|
||||||
fs.writeFileSync(
|
const result = { ...authInfo, ...info };
|
||||||
config.authConfigFile,
|
await shareStore.updateAuthInfo(result);
|
||||||
JSON.stringify({ ...authInfo, ...info }),
|
await this.updateAuthDb({
|
||||||
);
|
type: AuthDataType.authConfig,
|
||||||
|
info: result,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNotificationMode(): Promise<NotificationInfo> {
|
public async getNotificationMode(): Promise<NotificationInfo> {
|
||||||
const doc = await this.getDb({ type: AuthDataType.notification });
|
const doc = await this.getDb({ type: AuthDataType.notification });
|
||||||
return (doc && doc.info) || {};
|
return (doc.info || {}) as NotificationInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
private async updateAuthDb(payload: SystemInfo): Promise<any> {
|
||||||
let doc = await AuthModel.findOne({ type: payload.type });
|
let doc = await SystemModel.findOne({ where: { type: payload.type } });
|
||||||
if (doc) {
|
if (doc) {
|
||||||
const updateResult = await AuthModel.update(payload, {
|
const updateResult = await SystemModel.update(payload, {
|
||||||
where: { id: doc.id },
|
where: { id: doc.id },
|
||||||
returning: true,
|
returning: true,
|
||||||
});
|
});
|
||||||
doc = updateResult[1][0];
|
doc = updateResult[1][0];
|
||||||
} else {
|
} else {
|
||||||
doc = await AuthModel.create(payload, { returning: true });
|
doc = await SystemModel.create(payload, { returning: true });
|
||||||
}
|
}
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<any> {
|
public async getDb(query: any): Promise<SystemInfo> {
|
||||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
const doc = await SystemModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as any);
|
if (!doc) {
|
||||||
|
throw new Error(`${JSON.stringify(query)} not found`);
|
||||||
|
}
|
||||||
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||||
@@ -354,4 +345,13 @@ export default class UserService {
|
|||||||
return { code: 400, message: '通知发送失败,请检查参数' };
|
return { code: 400, message: '通知发送失败,请检查参数' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async resetAuthInfo(info: Partial<AuthInfo>) {
|
||||||
|
const { retries, twoFactorActivated } = info;
|
||||||
|
const authInfo = await this.getAuthInfo();
|
||||||
|
await this.updateAuthInfo(authInfo, {
|
||||||
|
retries,
|
||||||
|
twoFactorActivated,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Dependence } from '../data/dependence';
|
||||||
|
import { ICron } from '../protos/cron';
|
||||||
|
|
||||||
|
export type Override<
|
||||||
|
T,
|
||||||
|
K extends Partial<{ [P in keyof T]: any }> | string,
|
||||||
|
> = K extends string
|
||||||
|
? Omit<T, K> & { [P in keyof T]: T[P] | unknown }
|
||||||
|
: Omit<T, keyof K> & K;
|
||||||
|
|
||||||
|
export type TCron = Override<Partial<ICron>, { id: string }>;
|
||||||
|
|
||||||
|
export interface IDependencyFn<T> {
|
||||||
|
(): Promise<T>;
|
||||||
|
dependency?: Dependence;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICronFn<T> {
|
||||||
|
(): Promise<T>;
|
||||||
|
cron?: TCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchedule {
|
||||||
|
schedule?: string;
|
||||||
|
name?: string;
|
||||||
|
command?: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IScheduleFn<T> {
|
||||||
|
(): Promise<T>;
|
||||||
|
schedule?: ISchedule;
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import PQueue, { QueueAddOptions } from 'p-queue-cjs';
|
||||||
|
import os from 'os';
|
||||||
|
import { AuthDataType, SystemModel } from '../data/system';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import { Dependence } from '../data/dependence';
|
||||||
|
import NotificationService from '../services/notify';
|
||||||
|
import {
|
||||||
|
ICronFn,
|
||||||
|
IDependencyFn,
|
||||||
|
ISchedule,
|
||||||
|
IScheduleFn,
|
||||||
|
TCron,
|
||||||
|
} from './interface';
|
||||||
|
import config from '../config';
|
||||||
|
import { credentials } from '@grpc/grpc-js';
|
||||||
|
import { ApiClient } from '../protos/api';
|
||||||
|
|
||||||
|
class TaskLimit {
|
||||||
|
private dependenyLimit = new PQueue({ concurrency: 1 });
|
||||||
|
private queuedDependencyIds = new Set<number>([]);
|
||||||
|
private queuedCrons = new Map<string, ICronFn<any>[]>();
|
||||||
|
private repeatCronNotifyMap = new Map<string, number>();
|
||||||
|
private updateLogLimit = new PQueue({ concurrency: 1 });
|
||||||
|
private cronLimit = new PQueue({
|
||||||
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
|
});
|
||||||
|
private manualCronoLimit = new PQueue({
|
||||||
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
|
});
|
||||||
|
private subscriptionLimit = new PQueue({
|
||||||
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
|
});
|
||||||
|
private scriptLimit = new PQueue({
|
||||||
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
|
});
|
||||||
|
private systemLimit = new PQueue({
|
||||||
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
|
});
|
||||||
|
private client = new ApiClient(
|
||||||
|
`0.0.0.0:${config.cronPort}`,
|
||||||
|
credentials.createInsecure(),
|
||||||
|
{ 'grpc.enable_http_proxy': 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
get cronLimitActiveCount() {
|
||||||
|
return this.cronLimit.pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
get cronLimitPendingCount() {
|
||||||
|
return this.cronLimit.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
get firstDependencyId() {
|
||||||
|
return [...this.queuedDependencyIds.values()][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private notificationService: NotificationService = new NotificationService();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.setCustomLimit();
|
||||||
|
this.handleEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleEvents() {
|
||||||
|
this.cronLimit.on('add', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('active', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][开始处理任务] 运行中任务数: ${
|
||||||
|
this.cronLimitActiveCount + 1
|
||||||
|
}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('completed', (param) => {
|
||||||
|
Logger.info(`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('error', (error) => {
|
||||||
|
Logger.error(`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('next', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务处理结束] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('idle', () => {
|
||||||
|
Logger.info(`[schedule][任务队列] 空闲中...`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeQueuedDependency(dependency: Dependence) {
|
||||||
|
if (this.queuedDependencyIds.has(dependency.id!)) {
|
||||||
|
this.queuedDependencyIds.delete(dependency.id!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeQueuedCron(id: string) {
|
||||||
|
if (this.queuedCrons.has(id)) {
|
||||||
|
const runs = this.queuedCrons.get(id);
|
||||||
|
if (runs && runs.length > 0) {
|
||||||
|
runs.pop();
|
||||||
|
this.queuedCrons.set(id, runs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setCustomLimit(limit?: number) {
|
||||||
|
if (limit) {
|
||||||
|
this.cronLimit.concurrency = limit;
|
||||||
|
this.manualCronoLimit.concurrency = limit;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await SystemModel.sync();
|
||||||
|
const doc = await SystemModel.findOne({
|
||||||
|
where: { type: AuthDataType.systemConfig },
|
||||||
|
});
|
||||||
|
if (doc?.info?.cronConcurrency) {
|
||||||
|
this.cronLimit.concurrency = doc.info.cronConcurrency;
|
||||||
|
this.manualCronoLimit.concurrency = doc.info.cronConcurrency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runWithCronLimit<T>(
|
||||||
|
cron: TCron,
|
||||||
|
fn: ICronFn<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
fn.cron = cron;
|
||||||
|
let runs = this.queuedCrons.get(cron.id);
|
||||||
|
const result = runs?.length ? [...runs, fn] : [fn];
|
||||||
|
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
|
||||||
|
if (result?.length > 5) {
|
||||||
|
if (repeatTimes < 3) {
|
||||||
|
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
|
||||||
|
this.client.systemNotify(
|
||||||
|
{
|
||||||
|
title: '任务重复运行',
|
||||||
|
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
|
||||||
|
},
|
||||||
|
(err, res) => {
|
||||||
|
if (err) {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][任务重复运行] 通知失败 ${JSON.stringify(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Logger.warn(`[schedule][任务重复运行] 参数 ${JSON.stringify(cron)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.queuedCrons.set(cron.id, result);
|
||||||
|
return this.cronLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async manualRunWithCronLimit<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
return this.manualCronoLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runWithSubscriptionLimit<T>(
|
||||||
|
schedule: TCron,
|
||||||
|
fn: IScheduleFn<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
fn.schedule = schedule;
|
||||||
|
return this.subscriptionLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runWithSystemLimit<T>(
|
||||||
|
schedule: TCron,
|
||||||
|
fn: IScheduleFn<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
fn.schedule = schedule;
|
||||||
|
return this.systemLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runWithScriptLimit<T>(
|
||||||
|
schedule: ISchedule,
|
||||||
|
fn: IScheduleFn<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
fn.schedule = schedule;
|
||||||
|
return this.scriptLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public runDependeny<T>(
|
||||||
|
dependency: Dependence,
|
||||||
|
fn: IDependencyFn<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
this.queuedDependencyIds.add(dependency.id!);
|
||||||
|
fn.dependency = dependency;
|
||||||
|
return this.dependenyLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateDepLog<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
return this.updateLogLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new TaskLimit();
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { spawn } from 'cross-spawn';
|
||||||
|
import taskLimit from './pLimit';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import { ICron } from '../protos/cron';
|
||||||
|
|
||||||
|
export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
||||||
|
return taskLimit.runWithCronLimit(cron, () => {
|
||||||
|
return new Promise(async (resolve: any) => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][开始执行任务] 参数 ${JSON.stringify({
|
||||||
|
...cron,
|
||||||
|
command: cmd,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
cp.stderr.on('data', (data) => {
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
cmd,
|
||||||
|
data.toString(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
Logger.error(
|
||||||
|
'[schedule][创建任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
cmd,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code) => {
|
||||||
|
taskLimit.removeQueuedCron(cron.id);
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
|
||||||
|
JSON.stringify({
|
||||||
|
...cron,
|
||||||
|
command: cmd,
|
||||||
|
}),
|
||||||
|
code,
|
||||||
|
);
|
||||||
|
resolve({ ...cron, command: cmd, pid: cp.pid, code });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { AuthInfo } from '../data/system';
|
||||||
|
import { App } from '../data/open';
|
||||||
|
import Keyv from 'keyv';
|
||||||
|
import KeyvSqlite from '@keyv/sqlite';
|
||||||
|
import config from '../config';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export enum EKeyv {
|
||||||
|
'apps' = 'apps',
|
||||||
|
'authInfo' = 'authInfo',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKeyvStore {
|
||||||
|
apps: App[];
|
||||||
|
authInfo: AuthInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyvSqlite = new KeyvSqlite(path.join(config.dbPath, 'keyv.sqlite'));
|
||||||
|
export const keyvStore = new Keyv<IKeyvStore>({ store: keyvSqlite });
|
||||||
|
|
||||||
|
export const shareStore = {
|
||||||
|
getAuthInfo() {
|
||||||
|
return keyvStore.get<IKeyvStore['authInfo']>(EKeyv.authInfo);
|
||||||
|
},
|
||||||
|
updateAuthInfo(value: IKeyvStore['authInfo']) {
|
||||||
|
return keyvStore.set<IKeyvStore['authInfo']>(EKeyv.authInfo, value);
|
||||||
|
},
|
||||||
|
getApps() {
|
||||||
|
return keyvStore.get<IKeyvStore['apps']>(EKeyv.apps);
|
||||||
|
},
|
||||||
|
updateApps(apps: App[]) {
|
||||||
|
return keyvStore.set<IKeyvStore['apps']>(EKeyv.apps, apps);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { lock } from 'proper-lockfile';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { writeFile, open, chmod } from 'fs/promises';
|
||||||
|
import { fileExist } from '../config/util';
|
||||||
|
|
||||||
|
function getUniqueLockPath(filePath: string) {
|
||||||
|
const sanitizedPath = filePath
|
||||||
|
.replace(/[<>:"/\\|?*]/g, '_')
|
||||||
|
.replace(/^_/, '');
|
||||||
|
return path.join(os.tmpdir(), `${sanitizedPath}.ql_lock`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeFileWithLock(
|
||||||
|
filePath: string,
|
||||||
|
content: string | Buffer,
|
||||||
|
options: Parameters<typeof writeFile>[2] = {},
|
||||||
|
) {
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
options = { encoding: options };
|
||||||
|
}
|
||||||
|
if (!(await fileExist(filePath))) {
|
||||||
|
const fileHandle = await open(filePath, 'w');
|
||||||
|
fileHandle.close();
|
||||||
|
}
|
||||||
|
const lockfilePath = getUniqueLockPath(filePath);
|
||||||
|
|
||||||
|
const release = await lock(filePath, {
|
||||||
|
retries: {
|
||||||
|
retries: 10,
|
||||||
|
factor: 2,
|
||||||
|
minTimeout: 100,
|
||||||
|
maxTimeout: 3000,
|
||||||
|
},
|
||||||
|
lockfilePath,
|
||||||
|
});
|
||||||
|
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||||
|
if (options?.mode) {
|
||||||
|
await chmod(filePath, options.mode);
|
||||||
|
}
|
||||||
|
await release();
|
||||||
|
}
|
||||||
+3
-5
@@ -5,6 +5,8 @@ import LoggerInstance from './loaders/logger';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import { writeFileWithLock } from './shared/utils';
|
||||||
|
|
||||||
const tokenFile = path.join(config.configPath, 'token.json');
|
const tokenFile = path.join(config.configPath, 'token.json');
|
||||||
|
|
||||||
@@ -24,11 +26,7 @@ async function getToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function writeFile(data: any) {
|
async function writeFile(data: any) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
await writeFileWithLock(tokenFile, `${JSON.stringify(data)}${os.EOL}`);
|
||||||
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken();
|
getToken();
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'reflect-metadata'; // We need this in order to use @Decorators
|
||||||
|
import config from './config';
|
||||||
|
import express from 'express';
|
||||||
|
import depInjectorLoader from './loaders/depInjector';
|
||||||
|
import Logger from './loaders/logger';
|
||||||
|
|
||||||
|
|
||||||
|
async function startServer() {
|
||||||
|
const app = express();
|
||||||
|
depInjectorLoader();
|
||||||
|
|
||||||
|
await require('./loaders/update').default({ app });
|
||||||
|
|
||||||
|
app
|
||||||
|
.listen(config.updatePort, '0.0.0.0', () => {
|
||||||
|
Logger.debug(`✌️ 更新服务启动成功!`);
|
||||||
|
console.debug(`✌️ 更新服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
Logger.error(err);
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { CommandModule } from 'yargs';
|
|
||||||
export const updateCommand: CommandModule = {
|
|
||||||
command: 'update',
|
|
||||||
describe: 'Update and restart qinglong',
|
|
||||||
builder: (yargs) => {
|
|
||||||
return yargs.option('repositority', {
|
|
||||||
type: 'string',
|
|
||||||
alias: 'r',
|
|
||||||
describe: `Specify the release warehouse address of the package`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handler: async (argv) => {},
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import * as yargs from 'yargs';
|
|
||||||
import { green, red } from 'chalk';
|
|
||||||
import { updateCommand } from './commands/update';
|
|
||||||
|
|
||||||
yargs
|
|
||||||
.usage('Usage: ql [command] <options>')
|
|
||||||
.command(updateCommand)
|
|
||||||
.fail((err) => {
|
|
||||||
console.error(`${red(err)}`);
|
|
||||||
})
|
|
||||||
.alias('h', 'help')
|
|
||||||
.showHelp()
|
|
||||||
.recommendCommands().argv;
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
FROM python:3.10-alpine3.18 AS builder
|
||||||
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||||
|
RUN set -x \
|
||||||
|
&& apk update \
|
||||||
|
&& apk add nodejs npm git \
|
||||||
|
&& npm i -g pnpm@8.3.1 pm2 ts-node \
|
||||||
|
&& cd /tmp/build \
|
||||||
|
&& pnpm install --prod
|
||||||
|
|
||||||
|
FROM python:3.10-alpine
|
||||||
|
|
||||||
|
ARG QL_MAINTAINER="whyour"
|
||||||
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
|
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||||
|
ARG QL_BRANCH=develop
|
||||||
|
|
||||||
|
ENV PNPM_HOME=/root/.local/share/pnpm \
|
||||||
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules \
|
||||||
|
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
|
||||||
|
LANG=C.UTF-8 \
|
||||||
|
SHELL=/bin/bash \
|
||||||
|
PS1="\u@\h:\w \$ " \
|
||||||
|
QL_DIR=/ql \
|
||||||
|
QL_BRANCH=${QL_BRANCH}
|
||||||
|
|
||||||
|
VOLUME /ql/data
|
||||||
|
|
||||||
|
EXPOSE 5700
|
||||||
|
|
||||||
|
COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
|
||||||
|
COPY --from=builder /usr/local/bin/. /usr/local/bin/
|
||||||
|
|
||||||
|
RUN set -x \
|
||||||
|
&& apk update -f \
|
||||||
|
&& apk upgrade \
|
||||||
|
&& apk --no-cache add -f bash \
|
||||||
|
coreutils \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
tzdata \
|
||||||
|
perl \
|
||||||
|
openssl \
|
||||||
|
nginx \
|
||||||
|
nodejs \
|
||||||
|
jq \
|
||||||
|
openssh \
|
||||||
|
procps \
|
||||||
|
netcat-openbsd \
|
||||||
|
unzip \
|
||||||
|
npm \
|
||||||
|
&& rm -rf /var/cache/apk/* \
|
||||||
|
&& apk update \
|
||||||
|
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
|
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||||
|
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||||
|
&& git config --global user.name "qinglong" \
|
||||||
|
&& git config --global http.postBuffer 524288000 \
|
||||||
|
&& rm -rf /root/.pnpm-store \
|
||||||
|
&& rm -rf /root/.local/share/pnpm/store \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& ulimit -c 0 \
|
||||||
|
&& pip3 install requests
|
||||||
|
|
||||||
|
ARG SOURCE_COMMIT
|
||||||
|
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||||
|
&& cd ${QL_DIR} \
|
||||||
|
&& cp -f .env.example .env \
|
||||||
|
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||||
|
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||||
|
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||||
|
&& mkdir -p ${QL_DIR}/static \
|
||||||
|
&& cp -rf /static/* ${QL_DIR}/static \
|
||||||
|
&& rm -rf /static
|
||||||
|
|
||||||
|
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||||
|
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
+72
-49
@@ -1,4 +1,13 @@
|
|||||||
FROM python:alpine
|
FROM python:3.11-alpine3.18 AS builder
|
||||||
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||||
|
RUN set -x \
|
||||||
|
&& apk update \
|
||||||
|
&& apk add nodejs npm git \
|
||||||
|
&& npm i -g pnpm@8.3.1 pm2 ts-node \
|
||||||
|
&& cd /tmp/build \
|
||||||
|
&& pnpm install --prod
|
||||||
|
|
||||||
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
ARG QL_MAINTAINER="whyour"
|
ARG QL_MAINTAINER="whyour"
|
||||||
LABEL maintainer="${QL_MAINTAINER}"
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
@@ -6,55 +15,69 @@ ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
|||||||
ARG QL_BRANCH=develop
|
ARG QL_BRANCH=develop
|
||||||
|
|
||||||
ENV PNPM_HOME=/root/.local/share/pnpm \
|
ENV PNPM_HOME=/root/.local/share/pnpm \
|
||||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules \
|
||||||
LANG=zh_CN.UTF-8 \
|
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
|
||||||
SHELL=/bin/bash \
|
LANG=C.UTF-8 \
|
||||||
PS1="\u@\h:\w \$ " \
|
SHELL=/bin/bash \
|
||||||
QL_DIR=/ql \
|
PS1="\u@\h:\w \$ " \
|
||||||
QL_BRANCH=${QL_BRANCH}
|
QL_DIR=/ql \
|
||||||
|
QL_BRANCH=${QL_BRANCH}
|
||||||
|
|
||||||
|
VOLUME /ql/data
|
||||||
|
|
||||||
|
EXPOSE 5700
|
||||||
|
|
||||||
|
COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
|
||||||
|
COPY --from=builder /usr/local/bin/. /usr/local/bin/
|
||||||
|
|
||||||
|
RUN set -x \
|
||||||
|
&& apk update -f \
|
||||||
|
&& apk upgrade \
|
||||||
|
&& apk --no-cache add -f bash \
|
||||||
|
coreutils \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
tzdata \
|
||||||
|
perl \
|
||||||
|
openssl \
|
||||||
|
nginx \
|
||||||
|
nodejs \
|
||||||
|
jq \
|
||||||
|
openssh \
|
||||||
|
procps \
|
||||||
|
netcat-openbsd \
|
||||||
|
unzip \
|
||||||
|
npm \
|
||||||
|
&& rm -rf /var/cache/apk/* \
|
||||||
|
&& apk update \
|
||||||
|
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
|
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||||
|
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||||
|
&& git config --global user.name "qinglong" \
|
||||||
|
&& git config --global http.postBuffer 524288000 \
|
||||||
|
&& rm -rf /root/.pnpm-store \
|
||||||
|
&& rm -rf /root/.local/share/pnpm/store \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& ulimit -c 0 \
|
||||||
|
&& pip3 install requests
|
||||||
|
|
||||||
|
ARG SOURCE_COMMIT
|
||||||
|
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||||
|
&& cd ${QL_DIR} \
|
||||||
|
&& cp -f .env.example .env \
|
||||||
|
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||||
|
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||||
|
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||||
|
&& mkdir -p ${QL_DIR}/static \
|
||||||
|
&& cp -rf /static/* ${QL_DIR}/static \
|
||||||
|
&& rm -rf /static
|
||||||
|
|
||||||
|
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
WORKDIR ${QL_DIR}
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
RUN set -x \
|
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
|
||||||
&& apk update -f \
|
|
||||||
&& apk upgrade \
|
|
||||||
&& apk --no-cache add -f bash \
|
|
||||||
coreutils \
|
|
||||||
moreutils \
|
|
||||||
git \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
tzdata \
|
|
||||||
perl \
|
|
||||||
openssl \
|
|
||||||
nginx \
|
|
||||||
nodejs \
|
|
||||||
jq \
|
|
||||||
openssh \
|
|
||||||
npm \
|
|
||||||
&& rm -rf /var/cache/apk/* \
|
|
||||||
&& apk update \
|
|
||||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
|
||||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
|
||||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
|
||||||
&& git config --global user.name "qinglong" \
|
|
||||||
&& git config --global http.postBuffer 524288000 \
|
|
||||||
&& npm install -g pnpm \
|
|
||||||
&& pnpm add -g pm2 ts-node typescript tslib \
|
|
||||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
|
||||||
&& cd ${QL_DIR} \
|
|
||||||
&& cp -f .env.example .env \
|
|
||||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
|
||||||
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
|
||||||
&& pnpm install --prod \
|
|
||||||
&& rm -rf /root/.pnpm-store \
|
|
||||||
&& rm -rf /root/.local/share/pnpm/store \
|
|
||||||
&& rm -rf /root/.cache \
|
|
||||||
&& rm -rf /root/.npm \
|
|
||||||
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
|
||||||
&& mkdir -p ${QL_DIR}/static \
|
|
||||||
&& cp -rf /static/* ${QL_DIR}/static \
|
|
||||||
&& rm -rf /static
|
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
image: whyour/qinglong:latest
|
image: whyour/qinglong:latest # 基于 Debian 的版本:whyour/qinglong:debian
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/ql/data
|
- ./data:/ql/data
|
||||||
ports:
|
ports:
|
||||||
- "0.0.0.0:5700:5700"
|
- "5700:5700"
|
||||||
|
environment:
|
||||||
|
QlBaseUrl: '/' # 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
+11
-28
@@ -2,59 +2,42 @@
|
|||||||
|
|
||||||
dir_shell=/ql/shell
|
dir_shell=/ql/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
link_shell
|
. $dir_shell/env.sh
|
||||||
|
|
||||||
export isFirstStartServer=true
|
|
||||||
|
|
||||||
echo -e "======================1. 检测配置文件========================\n"
|
echo -e "======================1. 检测配置文件========================\n"
|
||||||
|
import_config "$@"
|
||||||
make_dir /etc/nginx/conf.d
|
make_dir /etc/nginx/conf.d
|
||||||
make_dir /run/nginx
|
make_dir /run/nginx
|
||||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
init_nginx
|
||||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
fix_config
|
||||||
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
|
|
||||||
pm2 l &>/dev/null
|
pm2 l &>/dev/null
|
||||||
|
|
||||||
patch_version &>/dev/null
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo -e "======================2. 安装依赖========================\n"
|
echo -e "======================2. 安装依赖========================\n"
|
||||||
update_depend
|
patch_version
|
||||||
echo
|
|
||||||
|
|
||||||
echo -e "======================3. 启动nginx========================\n"
|
echo -e "======================3. 启动nginx========================\n"
|
||||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||||
echo -e "nginx启动成功...\n"
|
echo -e "nginx启动成功...\n"
|
||||||
|
|
||||||
echo -e "======================4. 启动面板监控========================\n"
|
echo -e "======================4. 启动pm2服务========================\n"
|
||||||
pm2 delete public &>/dev/null
|
reload_update
|
||||||
pm2 start $dir_static/build/public.js -n public --source-map-support --time
|
reload_pm2
|
||||||
echo -e "监控服务启动成功...\n"
|
|
||||||
|
|
||||||
echo -e "======================5. 启动控制面板========================\n"
|
|
||||||
pm2 delete panel &>/dev/null
|
|
||||||
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
|
|
||||||
echo -e "控制面板启动成功...\n"
|
|
||||||
|
|
||||||
echo -e "======================6. 启动定时任务========================\n"
|
|
||||||
pm2 delete schedule &>/dev/null
|
|
||||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
|
|
||||||
echo -e "定时任务启动成功...\n"
|
|
||||||
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
echo -e "======================7. 启动bot========================\n"
|
echo -e "======================5. 启动bot========================\n"
|
||||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
nohup ql bot >$dir_log/bot.log 2>&1 &
|
||||||
echo -e "bot后台启动中...\n"
|
echo -e "bot后台启动中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EnableExtraShell == true ]]; then
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
echo -e "======================8. 执行自定义脚本========================\n"
|
echo -e "====================6. 执行自定义脚本========================\n"
|
||||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
nohup ql extra >$dir_log/extra.log 2>&1 &
|
||||||
echo -e "自定义脚本后台执行中...\n"
|
echo -e "自定义脚本后台执行中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "############################################################\n"
|
echo -e "############################################################\n"
|
||||||
echo -e "容器启动成功..."
|
echo -e "容器启动成功..."
|
||||||
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
|
|
||||||
echo -e "############################################################\n"
|
echo -e "############################################################\n"
|
||||||
|
|
||||||
crond -f >/dev/null
|
crond -f >/dev/null
|
||||||
|
|||||||
+41
-8
@@ -6,38 +6,69 @@ upstream publicApi {
|
|||||||
server 0.0.0.0:5400;
|
server 0.0.0.0:5400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstream updateApi {
|
||||||
|
server 0.0.0.0:5300;
|
||||||
|
}
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
map $http_upgrade $connection_upgrade {
|
||||||
default keep-alive;
|
default keep-alive;
|
||||||
'websocket' upgrade;
|
'websocket' upgrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 5700;
|
IPV4_CONFIG
|
||||||
root /ql/static/dist;
|
IPV6_CONFIG
|
||||||
ssl_session_timeout 5m;
|
ssl_session_timeout 5m;
|
||||||
|
|
||||||
location QL_BASE_URL/api/public/ {
|
location QL_BASE_URLapi/update/ {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass http://publicApi/api/public/;
|
proxy_pass http://updateApi/api/;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_redirect default;
|
||||||
|
proxy_connect_timeout 1800;
|
||||||
|
proxy_send_timeout 1800;
|
||||||
|
proxy_read_timeout 1800;
|
||||||
}
|
}
|
||||||
|
|
||||||
location QL_BASE_URL/api/ {
|
location QL_BASE_URLapi/public/ {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass http://publicApi/api/;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_redirect default;
|
||||||
|
proxy_connect_timeout 1800;
|
||||||
|
proxy_send_timeout 1800;
|
||||||
|
proxy_read_timeout 1800;
|
||||||
|
}
|
||||||
|
|
||||||
|
location QL_BASE_URLapi/ {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass http://baseApi/api/;
|
proxy_pass http://baseApi/api/;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_redirect default;
|
||||||
|
proxy_connect_timeout 1800;
|
||||||
|
proxy_send_timeout 1800;
|
||||||
|
proxy_read_timeout 1800;
|
||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
location QL_BASE_URL/open/ {
|
location QL_BASE_URLopen/ {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass http://baseApi/open/;
|
proxy_pass http://baseApi/open/;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_redirect default;
|
||||||
|
proxy_connect_timeout 1800;
|
||||||
|
proxy_send_timeout 1800;
|
||||||
|
proxy_read_timeout 1800;
|
||||||
}
|
}
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
@@ -48,10 +79,12 @@ server {
|
|||||||
gzip_comp_level 6;
|
gzip_comp_level 6;
|
||||||
gzip_buffers 16 8k;
|
gzip_buffers 16 8k;
|
||||||
gzip_http_version 1.0;
|
gzip_http_version 1.0;
|
||||||
|
QL_ROOT_CONFIG
|
||||||
|
|
||||||
location QL_BASE_URL/ {
|
location QL_BASE_URL_LOCATION {
|
||||||
|
QL_ALIAS_CONFIG
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
try_files $uri $uri/ QL_BASE_URL/index.html;
|
try_files $uri QL_BASE_URLindex.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ .*\.(html)$ {
|
location ~ .*\.(html)$ {
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ http {
|
|||||||
|
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
|
|
||||||
client_max_body_size 20m;
|
client_max_body_size 4096m;
|
||||||
client_body_buffer_size 20m;
|
client_body_buffer_size 20m;
|
||||||
|
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'schedule',
|
||||||
|
max_restarts: 10,
|
||||||
|
kill_timeout: 15000,
|
||||||
|
wait_ready: true,
|
||||||
|
listen_timeout: 10000,
|
||||||
|
source_map_support: true,
|
||||||
|
time: true,
|
||||||
|
script: 'static/build/schedule/index.js',
|
||||||
|
env: {
|
||||||
|
http_proxy: '',
|
||||||
|
https_proxy: '',
|
||||||
|
HTTP_PROXY: '',
|
||||||
|
HTTPS_PROXY: '',
|
||||||
|
all_proxy: '',
|
||||||
|
ALL_PROXY: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'public',
|
||||||
|
max_restarts: 10,
|
||||||
|
kill_timeout: 15000,
|
||||||
|
wait_ready: true,
|
||||||
|
listen_timeout: 10000,
|
||||||
|
source_map_support: true,
|
||||||
|
time: true,
|
||||||
|
script: 'static/build/public.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'panel',
|
||||||
|
max_restarts: 10,
|
||||||
|
kill_timeout: 15000,
|
||||||
|
wait_ready: true,
|
||||||
|
listen_timeout: 10000,
|
||||||
|
source_map_support: true,
|
||||||
|
time: true,
|
||||||
|
script: 'static/build/app.js',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"watch": ["back", ".env"],
|
"watch": ["back", ".env"],
|
||||||
"ext": "js,ts,json",
|
"ext": "js,ts,json",
|
||||||
"ignore": ["src/**/*.spec.ts"],
|
"exec": "ts-node -P tsconfig.back.json ./back/app.ts"
|
||||||
"exec": "ts-node --transpile-only ./back/app.ts"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'update',
|
||||||
|
max_restarts: 10,
|
||||||
|
kill_timeout: 15000,
|
||||||
|
wait_ready: true,
|
||||||
|
listen_timeout: 10000,
|
||||||
|
time: true,
|
||||||
|
script: 'static/build/update.js',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
+86
-49
@@ -2,14 +2,19 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently -n w: npm:start:*",
|
"start": "concurrently -n w: npm:start:*",
|
||||||
"start:front": "max dev",
|
"start:update": "ts-node -P tsconfig.back.json ./back/update.ts",
|
||||||
|
"start:public": "ts-node -P tsconfig.back.json ./back/public.ts",
|
||||||
|
"start:rpc": "ts-node -P tsconfig.back.json ./back/schedule/index.ts",
|
||||||
"start:back": "nodemon",
|
"start:back": "nodemon",
|
||||||
"start:public": "ts-node back/public.ts",
|
"start:front": "max dev",
|
||||||
"build:front": "max build",
|
"build:front": "max build",
|
||||||
"build:back": "tsc -p tsconfig.back.json",
|
"build:back": "tsc -p tsconfig.back.json",
|
||||||
"panel": "npm run build:back && node static/build/app.js",
|
"panel": "npm run build:back && node static/build/app.js",
|
||||||
"schedule": "npm run build:back && node static/build/schedule.js",
|
"schedule": "npm run build:back && node static/build/schedule/index.js",
|
||||||
"public": "npm run build:back && node static/build/public.js",
|
"public": "npm run build:back && node static/build/public.js",
|
||||||
|
"update": "npm run build:back && node static/build/update.js",
|
||||||
|
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
|
||||||
|
"gen:api": "python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./back/protos/api.proto",
|
||||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||||
"postinstall": "max setup 2>/dev/null || true",
|
"postinstall": "max setup 2>/dev/null || true",
|
||||||
"test": "umi-test",
|
"test": "umi-test",
|
||||||
@@ -42,103 +47,135 @@
|
|||||||
"monaco-editor",
|
"monaco-editor",
|
||||||
"rc-field-form",
|
"rc-field-form",
|
||||||
"@types/lodash.merge",
|
"@types/lodash.merge",
|
||||||
"rollup"
|
"rollup",
|
||||||
|
"styled-components"
|
||||||
],
|
],
|
||||||
"allowedVersions": {
|
"allowedVersions": {
|
||||||
"react": "18",
|
"react": "18",
|
||||||
"react-dom": "18",
|
"react-dom": "18",
|
||||||
"dva-core": "2"
|
"dva-core": "2"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.12.3",
|
||||||
|
"@grpc/proto-loader": "^0.7.13",
|
||||||
"@otplib/preset-default": "^12.0.1",
|
"@otplib/preset-default": "^12.0.1",
|
||||||
"@sentry/node": "^7.12.1",
|
"body-parser": "^1.20.3",
|
||||||
"@sentry/tracing": "^7.12.1",
|
"celebrate": "^15.0.3",
|
||||||
"body-parser": "^1.19.2",
|
"chokidar": "^4.0.1",
|
||||||
"celebrate": "^15.0.1",
|
|
||||||
"chokidar": "^3.5.3",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron-parser": "^4.2.1",
|
"cron-parser": "^4.9.0",
|
||||||
"dayjs": "^1.11.2",
|
"cross-spawn": "^7.0.6",
|
||||||
"dotenv": "^16.0.0",
|
"dayjs": "^1.11.13",
|
||||||
"express": "^4.17.3",
|
"dotenv": "^16.4.6",
|
||||||
"express-jwt": "^6.1.1",
|
"express": "^4.21.2",
|
||||||
"express-urlrewrite": "^1.4.0",
|
"express-jwt": "^8.4.1",
|
||||||
|
"express-rate-limit": "^7.4.1",
|
||||||
|
"express-urlrewrite": "^2.0.3",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
"hpagent": "^0.1.2",
|
"hpagent": "^1.2.0",
|
||||||
|
"http-proxy-middleware": "^3.0.3",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"js-yaml": "^4.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"multer": "^1.4.4",
|
"multer": "1.4.5-lts.1",
|
||||||
"nedb": "^1.8.0",
|
|
||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.9.16",
|
||||||
"p-queue": "7.2.0",
|
"p-queue-cjs": "7.3.4",
|
||||||
"reflect-metadata": "^0.1.13",
|
"protobufjs": "^7.4.0",
|
||||||
"sequelize": "^6.20.1",
|
"pstree.remy": "^1.1.8",
|
||||||
"serve-handler": "^6.1.3",
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"sequelize": "^6.37.5",
|
||||||
|
"serve-handler": "^6.1.6",
|
||||||
"sockjs": "^0.3.24",
|
"sockjs": "^0.3.24",
|
||||||
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
|
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
|
||||||
"toad-scheduler": "^1.6.0",
|
"toad-scheduler": "^3.0.1",
|
||||||
"typedi": "^0.10.0",
|
"typedi": "^0.10.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^11.0.3",
|
||||||
"winston": "^3.6.0",
|
"winston": "^3.17.0",
|
||||||
"yargs": "^17.3.1"
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
|
"request-ip": "3.3.0",
|
||||||
|
"ip2region": "2.3.0",
|
||||||
|
"keyv": "^5.2.3",
|
||||||
|
"@keyv/sqlite": "^4.0.1",
|
||||||
|
"proper-lockfile": "^4.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^4.7.0",
|
"moment": "2.30.1",
|
||||||
"@ant-design/pro-layout": "^6.33.1",
|
"@ant-design/icons": "^5.0.1",
|
||||||
|
"@ant-design/pro-layout": "6.38.22",
|
||||||
|
"@codemirror/view": "^6.34.1",
|
||||||
|
"@codemirror/state": "^6.4.1",
|
||||||
"@monaco-editor/react": "4.2.1",
|
"@monaco-editor/react": "4.2.1",
|
||||||
"@sentry/react": "^7.12.1",
|
"@react-hook/resize-observer": "^2.0.2",
|
||||||
|
"react-router-dom": "6.26.1",
|
||||||
"@types/body-parser": "^1.19.2",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/cross-spawn": "^6.0.2",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/express-jwt": "^6.0.4",
|
"@types/express-jwt": "^6.0.4",
|
||||||
|
"@types/file-saver": "2.0.2",
|
||||||
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/jsonwebtoken": "^8.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/lodash": "^4.14.179",
|
"@types/lodash": "^4.14.185",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/nedb": "^1.8.12",
|
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/node-schedule": "^1.3.2",
|
"@types/node-schedule": "^1.3.2",
|
||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/qrcode.react": "^1.0.2",
|
"@types/qrcode.react": "^1.0.2",
|
||||||
"@types/react": "^18.0.20",
|
"@types/react": "^18.0.20",
|
||||||
|
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/serve-handler": "^6.1.1",
|
"@types/serve-handler": "^6.1.1",
|
||||||
"@types/sockjs": "^0.3.33",
|
"@types/sockjs": "^0.3.33",
|
||||||
"@types/sockjs-client": "^1.5.1",
|
"@types/sockjs-client": "^1.5.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@umijs/max": "^4.0.21",
|
"@types/request-ip": "0.0.41",
|
||||||
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
|
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||||
|
"@uiw/react-codemirror": "^4.21.9",
|
||||||
|
"@umijs/max": "^4.4.4",
|
||||||
"@umijs/ssr-darkreader": "^4.9.45",
|
"@umijs/ssr-darkreader": "^4.9.45",
|
||||||
|
"ahooks": "^3.7.8",
|
||||||
"ansi-to-react": "^6.1.6",
|
"ansi-to-react": "^6.1.6",
|
||||||
"antd": "^4.23.0",
|
"antd": "^4.24.16",
|
||||||
"antd-img-crop": "^4.2.3",
|
"antd-img-crop": "^4.23.0",
|
||||||
"codemirror": "^5.65.2",
|
"axios": "^1.4.0",
|
||||||
"compression-webpack-plugin": "9.2.0",
|
"compression-webpack-plugin": "9.2.0",
|
||||||
"concurrently": "^7.0.0",
|
"concurrently": "^7.0.0",
|
||||||
"lint-staged": "^12.3.4",
|
"react-hotkeys-hook": "^4.6.1",
|
||||||
"nodemon": "^2.0.15",
|
"file-saver": "2.0.2",
|
||||||
|
"lint-staged": "^13.0.3",
|
||||||
|
"monaco-editor": "0.33.0",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
|
"pretty-bytes": "6.1.1",
|
||||||
"qiniu": "^7.4.0",
|
"qiniu": "^7.4.0",
|
||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
"rc-tween-one": "^3.0.6",
|
"rc-tween-one": "^3.0.6",
|
||||||
"react": "18.2.0",
|
"rc-virtual-list": "3.15.0",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react": "18.3.1",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.3.1",
|
||||||
|
"react-intl-universal": "^2.12.0",
|
||||||
"react-split-pane": "^0.1.92",
|
"react-split-pane": "^0.1.92",
|
||||||
"sockjs-client": "^1.6.0",
|
"sockjs-client": "^1.6.0",
|
||||||
"ts-node": "^10.6.0",
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-proto": "^1.146.0",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "^4.6.2",
|
"typescript": "5.2.2",
|
||||||
"umi-request": "^1.4.0",
|
|
||||||
"vh-check": "^2.0.5",
|
"vh-check": "^2.0.5",
|
||||||
"webpack": "^5.70.0",
|
"virtualizedtableforantd4": "1.3.0",
|
||||||
"yorkie": "^2.0.0"
|
"yorkie": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+15424
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.8 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user