mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0977f9e37 | ||
|
|
e9b85347f4 | ||
|
|
325482cbb1 | ||
|
|
45e02a55c3 | ||
|
|
6b8ef75b51 | ||
|
|
f2535ece76 | ||
|
|
b2afa65cb1 | ||
|
|
3a62cc37ff | ||
|
|
93c69826ad | ||
|
|
70727d0e26 | ||
|
|
2578aae0ba | ||
|
|
616e56ea95 | ||
|
|
96ae20de68 | ||
|
|
6975ff4aa1 | ||
|
|
1d8a1e9c4a | ||
|
|
2719280cdc | ||
|
|
575e41172f | ||
|
|
bd50b1e976 | ||
|
|
dfb2572b4f | ||
|
|
7746b18023 | ||
|
|
05c738c7c6 | ||
|
|
e561e12356 | ||
|
|
595b28ba62 | ||
|
|
366caed74e | ||
|
|
2e26a8c24e | ||
|
|
c09c375066 | ||
|
|
4f93905507 | ||
|
|
f152136f0b | ||
|
|
c24d3d6e2f | ||
|
|
33d820acdb | ||
|
|
78ef75bbfe | ||
|
|
31541fb87e | ||
|
|
0c169e3f6c | ||
|
|
73c15cb3bc | ||
|
|
4373a2df49 | ||
|
|
cb004cfbf5 | ||
|
|
7f2d6d159c | ||
|
|
4bba024e4a | ||
|
|
acb43f3ad8 | ||
|
|
b160f914ad | ||
|
|
6116aa6d12 | ||
|
|
d43cee53c9 | ||
|
|
defea8ff9b | ||
|
|
b290b60d46 | ||
|
|
706b5e8108 | ||
|
|
b55c7d5747 | ||
|
|
6b8fd94f0c | ||
|
|
e18710a30a | ||
|
|
c0f0da4e96 | ||
|
|
49ef5cd61f | ||
|
|
99d7eaeff0 | ||
|
|
c8f5eb7c2e | ||
|
|
109e16e8fb | ||
|
|
555f6e04e8 | ||
|
|
bcde0a54bf | ||
|
|
eee7f25bc3 | ||
|
|
b10157b09a | ||
|
|
a09a21fa5d | ||
|
|
ec2918b427 | ||
|
|
57bfc19ecf | ||
|
|
aa110d4187 | ||
|
|
9e8febfc11 | ||
|
|
5905c2ce25 | ||
|
|
b03ef0d494 | ||
|
|
585f77ed49 | ||
|
|
1c580ea2f9 | ||
|
|
b1af6575b9 | ||
|
|
b24c5fdee1 | ||
|
|
a256ada847 | ||
|
|
a8cdab629d | ||
|
|
09cca0a920 | ||
|
|
6a0aa6aae2 | ||
|
|
565cc06c27 | ||
|
|
29caa4b770 | ||
|
|
953cf4795a | ||
|
|
31bc8395aa | ||
|
|
eb94b39e40 | ||
|
|
c16bbe0f9a | ||
|
|
b60caa60d5 | ||
|
|
1ea97e9206 | ||
|
|
32970bca71 | ||
|
|
1fc9f87eaa | ||
|
|
b2f5960097 | ||
|
|
1cfcc66548 | ||
|
|
60152a313f | ||
|
|
d50ff20ff8 | ||
|
|
3aed5ef955 | ||
|
|
61e0e8dbe2 | ||
|
|
1ba5d52ee7 | ||
|
|
03bc5bb882 | ||
|
|
8011b4b793 | ||
|
|
deb124ae23 | ||
|
|
a3114e40d1 | ||
|
|
0b9ecfc3a5 | ||
|
|
c32bb4157b | ||
|
|
7f9c9f05b8 | ||
|
|
906a074634 | ||
|
|
286ec0e714 | ||
|
|
0f81414497 | ||
|
|
b451831db2 | ||
|
|
ae13185319 | ||
|
|
f2bcbaefd2 | ||
|
|
96ddcf49c5 | ||
|
|
322a4e2729 | ||
|
|
8e48960253 | ||
|
|
3b419a8e96 | ||
|
|
bf1685bf79 | ||
|
|
3ab84428f0 | ||
|
|
fdda732c4c | ||
|
|
f46b30498b | ||
|
|
d7ee2576f2 | ||
|
|
d2c03bc298 | ||
|
|
6283e23d29 | ||
|
|
dc9923ac13 | ||
|
|
62ac80257a | ||
|
|
868452f13c | ||
|
|
6c368f7ea7 | ||
|
|
732d63d588 | ||
|
|
ae40947801 | ||
|
|
079d4dfa06 | ||
|
|
67c50bf622 | ||
|
|
f1e494b1fc | ||
|
|
f4f275490e | ||
|
|
baad96106f | ||
|
|
15fa7754b5 | ||
|
|
8f175f8447 | ||
|
|
9c45cfd7ed | ||
|
|
f2426c3f66 | ||
|
|
36ae6e460d | ||
|
|
7ae93efe07 | ||
|
|
8b97d92d0a | ||
|
|
e5c06335ec | ||
|
|
2bd8e49302 | ||
|
|
c0d401e296 | ||
|
|
e07d2b6639 | ||
|
|
220a87627d | ||
|
|
87bca2ac4e | ||
|
|
c320906149 | ||
|
|
27958a1a90 | ||
|
|
82c7011522 | ||
|
|
fa6d57d809 | ||
|
|
1060fc7476 | ||
|
|
56bc38e9e7 | ||
|
|
18be65d6fe | ||
|
|
3409085195 | ||
|
|
17e17ac077 | ||
|
|
c655e1da38 | ||
|
|
0ea158724f | ||
|
|
fb521498ff | ||
|
|
399550ccad | ||
|
|
c7c30c86f2 | ||
|
|
5f6c0882d6 | ||
|
|
6e05a4f3a5 | ||
|
|
ea8aa6a231 | ||
|
|
ae2570c677 | ||
|
|
c76d952d41 | ||
|
|
cdf9fb24be | ||
|
|
2131e16781 | ||
|
|
b0c8425639 | ||
|
|
f0869567de | ||
|
|
be2a19949b | ||
|
|
8627cccb58 | ||
|
|
4a8eaebf17 | ||
|
|
e7edd0a5a4 | ||
|
|
9ccbea0a04 | ||
|
|
ab715ef840 | ||
|
|
117822fe7c | ||
|
|
da922678a1 | ||
|
|
133e9e0e1f | ||
|
|
7cc6081d15 | ||
|
|
5a24f62a62 | ||
|
|
be597355a7 | ||
|
|
51afcb6c5b | ||
|
|
92ebb70eaa | ||
|
|
ab19f30342 | ||
|
|
fc9bc7bea2 | ||
|
|
793da29f22 | ||
|
|
5afe4da607 | ||
|
|
a7a56c03ce | ||
|
|
7639b588d8 | ||
|
|
1d536cdd53 | ||
|
|
9f08245b6d | ||
|
|
ff9bf20e57 | ||
|
|
b201730100 | ||
|
|
6ca098f9e2 | ||
|
|
e0eef05f5c | ||
|
|
6398e5ec70 | ||
|
|
6f93219c44 | ||
|
|
9b957ac019 | ||
|
|
e0d1b127aa | ||
|
|
64072b3435 | ||
|
|
f81f7720f0 | ||
|
|
69fab610e6 | ||
|
|
d693e1f9ea | ||
|
|
2a9ca67355 | ||
|
|
3fb01860d4 | ||
|
|
d92a46d09c | ||
|
|
ec605bfba4 | ||
|
|
d2f999a5e2 | ||
|
|
627f541f17 | ||
|
|
c457dca555 | ||
|
|
ff3ec7395e | ||
|
|
fdaba146ca | ||
|
|
31a6d30c23 | ||
|
|
fa3b185fd5 | ||
|
|
101d4fb497 | ||
|
|
ccd308f5b0 | ||
|
|
999d9f8e01 | ||
|
|
313db0d74e | ||
|
|
634e9705a7 | ||
|
|
c959cec738 | ||
|
|
95330e04a3 | ||
|
|
a099d09af7 | ||
|
|
a0d0197ad7 | ||
|
|
d49e711a34 | ||
|
|
e3b8cd75b9 | ||
|
|
7a240eaabb | ||
|
|
01564e96d3 | ||
|
|
03f4b5c219 | ||
|
|
99e1d44f7d | ||
|
|
8c55659c18 | ||
|
|
3ea980b2f8 | ||
|
|
f5fc987b51 | ||
|
|
9cfa4af2b2 | ||
|
|
3efdf1fa87 | ||
|
|
25fc832605 | ||
|
|
a7e404c5d5 | ||
|
|
90b27f0564 | ||
|
|
632440965d | ||
|
|
aedfac446b | ||
|
|
3372b0025a | ||
|
|
0112b92e77 | ||
|
|
0caf53b147 | ||
|
|
4d1354d5f4 | ||
|
|
fc4562eca2 | ||
|
|
5b048a5510 | ||
|
|
d23049b2fe | ||
|
|
caac5607b9 | ||
|
|
fb2e69b12d | ||
|
|
fc1838ff39 | ||
|
|
25689cf40f | ||
|
|
c746a3d215 | ||
|
|
548d924fa0 | ||
|
|
18a6be3ecd | ||
|
|
afef0cb72f | ||
|
|
1221b47eca | ||
|
|
a356eeb5da | ||
|
|
25bb0972b9 | ||
|
|
0914075cf6 | ||
|
|
74b258e1b6 | ||
|
|
27eb0a21e4 | ||
|
|
d77c6d50d8 | ||
|
|
6a64c18a7b | ||
|
|
b97ff714ae | ||
|
|
1f23eea805 | ||
|
|
cb0cb7df21 | ||
|
|
f3e8628664 | ||
|
|
4758471f9f | ||
|
|
c10bd5a549 | ||
|
|
d53faf96eb | ||
|
|
2307b8ed1a | ||
|
|
711af8d4a5 | ||
|
|
458c9c456b | ||
|
|
29069df48c | ||
|
|
300e84ae2c | ||
|
|
bf11768b92 | ||
|
|
91bd1d9263 | ||
|
|
ac40f9ff12 | ||
|
|
f930837220 | ||
|
|
1d5e3acc93 | ||
|
|
2aec1f9846 | ||
|
|
b56a681b98 | ||
|
|
2713419b32 | ||
|
|
764a3492cc | ||
|
|
29fc471ac4 | ||
|
|
54a597aad3 | ||
|
|
778961d0b6 | ||
|
|
14b01ac761 | ||
|
|
f87848c05b | ||
|
|
153e5a806d | ||
|
|
ee1b8a906f | ||
|
|
734685d45a | ||
|
|
e3b540c212 | ||
|
|
201f578bbc | ||
|
|
ce9aa369d2 | ||
|
|
c5c41c0859 | ||
|
|
d3009f82dd | ||
|
|
0eb9a1fdb0 | ||
|
|
83f6d9926a | ||
|
|
14134842b0 | ||
|
|
4c094c4710 | ||
|
|
3ac41075fd | ||
|
|
fdc97773d1 | ||
|
|
917a739595 | ||
|
|
21475f81ae | ||
|
|
a2a9a0560c | ||
|
|
b879846cdb | ||
|
|
a24011cd72 | ||
|
|
410dd2ecdc | ||
|
|
96b111b894 | ||
|
|
630572c8f9 | ||
|
|
6a381a6366 | ||
|
|
fd04f12677 | ||
|
|
86b8cd17a4 | ||
|
|
bb8ee01693 | ||
|
|
5fb0bca3af | ||
|
|
1e4d1f373d | ||
|
|
87c78d0fcc | ||
|
|
041d2b7947 | ||
|
|
5e7b90d995 | ||
|
|
fadc3dcb55 | ||
|
|
4eea091944 | ||
|
|
9f8d46f395 | ||
|
|
2c54a057c9 | ||
|
|
e1e5cacc6d | ||
|
|
edf9066967 | ||
|
|
7097d12b87 | ||
|
|
08aaeb6372 | ||
|
|
4310f80ec0 | ||
|
|
01577f6494 | ||
|
|
9aa570b2c6 | ||
|
|
d611d7d698 | ||
|
|
f05549b06c | ||
|
|
8c4db8dd40 | ||
|
|
ddee69d291 | ||
|
|
28960c9fab | ||
|
|
449301e519 | ||
|
|
28dab3dc2f | ||
|
|
7995d98db5 | ||
|
|
1f319f3533 | ||
|
|
7d2dbaa62d | ||
|
|
ed5999526b | ||
|
|
2409f75491 | ||
|
|
0c840095d1 | ||
|
|
3eaf0b4856 | ||
|
|
394735a5d9 | ||
|
|
5bfa10bfe7 | ||
|
|
3f12d9cbd5 | ||
|
|
54d89754df | ||
|
|
9470524615 | ||
|
|
b5faa6eaaa | ||
|
|
eab476cfe8 | ||
|
|
b773def012 | ||
|
|
a9f8b23a7a | ||
|
|
61bb6535fb | ||
|
|
eb0bdffe06 | ||
|
|
e129485285 | ||
|
|
aac7de172e | ||
|
|
e19d2412fc | ||
|
|
7cba2cf76c | ||
|
|
55d7a49dbc | ||
|
|
f695864fd8 | ||
|
|
ae4883fbe7 | ||
|
|
836dfd2861 | ||
|
|
6e0523c6d7 | ||
|
|
cd9ba084ae | ||
|
|
c8a0c0de67 | ||
|
|
5dd8d9df91 | ||
|
|
2dbee94e85 | ||
|
|
9001797037 | ||
|
|
5e7104d5a3 | ||
|
|
7c33b22d63 | ||
|
|
b7cb1e379c | ||
|
|
274a6ce52e | ||
|
|
8079e4e7b8 | ||
|
|
920efb8fab | ||
|
|
92aeb2b9da | ||
|
|
ced76cf343 | ||
|
|
3750d6ecd5 | ||
|
|
7444f30384 | ||
|
|
b0fd5ba47f | ||
|
|
cc098bc2e5 | ||
|
|
1cfcf8d194 | ||
|
|
d1e9f741d0 | ||
|
|
0cde9d07ee | ||
|
|
a266425348 | ||
|
|
6dbe02e321 | ||
|
|
39979879c9 | ||
|
|
ba3f604a5c | ||
|
|
9572582401 | ||
|
|
2bcef0ba75 | ||
|
|
31cd7c4007 | ||
|
|
1c4c1799b0 | ||
|
|
2d558df15e | ||
|
|
7b769da4e2 | ||
|
|
4e95d62933 | ||
|
|
83255280b9 | ||
|
|
2546cebdea | ||
|
|
f14a3fe0f1 | ||
|
|
c3eb845be4 | ||
|
|
1eef77dca5 | ||
|
|
d7d9f47fe5 | ||
|
|
fce6a996d9 | ||
|
|
48a484d7de | ||
|
|
4b81d3c612 | ||
|
|
21b6edd7eb | ||
|
|
9a43e022e9 | ||
|
|
62bef3f13d | ||
|
|
c87f5fa004 | ||
|
|
8d99f3ffbb | ||
|
|
100ca7b54b | ||
|
|
4a2624f12a | ||
|
|
21b01a85a8 | ||
|
|
e039ae436e | ||
|
|
b13a1a45f8 | ||
|
|
667fe2bbba | ||
|
|
f083871ca0 | ||
|
|
782e1ba235 | ||
|
|
b92c6edc4e | ||
|
|
bdf4657c73 | ||
|
|
8559438611 | ||
|
|
04045fd410 | ||
|
|
1c71a9d8a8 | ||
|
|
9a9ae9782c | ||
|
|
da2569850b | ||
|
|
b475035415 | ||
|
|
7baee27585 | ||
|
|
2cb2f1ce0a | ||
|
|
8c1407e2a9 | ||
|
|
7bcb113085 | ||
|
|
42dc27addf | ||
|
|
aac30398c4 | ||
|
|
ceae888372 | ||
|
|
61df7050db | ||
|
|
af5561cbbf | ||
|
|
bce30f922d | ||
|
|
d6ed394557 | ||
|
|
7b512a1681 | ||
|
|
7af0597cd5 | ||
|
|
2f5cf6d93c | ||
|
|
d672d2d104 | ||
|
|
c0d1fb1ef4 | ||
|
|
3c882ad0ce | ||
|
|
58acd2eb68 | ||
|
|
590473e7d2 | ||
|
|
21139157c8 | ||
|
|
8f8296ab64 | ||
|
|
40bf07d2a4 | ||
|
|
fd2b4fb86d | ||
|
|
783e867ef6 | ||
|
|
3873d9f978 | ||
|
|
7dece938c8 | ||
|
|
0842d66c79 | ||
|
|
d071e50d62 | ||
|
|
337f01aff1 | ||
|
|
5614e41eb0 | ||
|
|
4f0fdbe453 | ||
|
|
49c068db78 | ||
|
|
fbba44d482 | ||
|
|
10c07b0a09 | ||
|
|
20893698ba | ||
|
|
31de659ed3 | ||
|
|
6c08a16c66 | ||
|
|
21e7178f48 | ||
|
|
aefd267eb2 | ||
|
|
fb1a3980d4 | ||
|
|
cfc467f8a5 | ||
|
|
48120f012d | ||
|
|
c907a78437 | ||
|
|
915c0d09a9 | ||
|
|
80638862cc | ||
|
|
a7350948cd | ||
|
|
1e1be013b5 | ||
|
|
4062584eae | ||
|
|
abedddffa6 | ||
|
|
38858ccdb2 | ||
|
|
d56e6359d0 | ||
|
|
5886991464 | ||
|
|
838326616b | ||
|
|
fcf80fe946 | ||
|
|
2e63a7056c | ||
|
|
7c807e3ec8 | ||
|
|
b558aa0934 | ||
|
|
38757758cb | ||
|
|
84dd3d8fd1 | ||
|
|
19423c2d30 | ||
|
|
98bba7d7fa | ||
|
|
dcaa9ce000 | ||
|
|
3cae44a136 | ||
|
|
df8a3d2692 | ||
|
|
56e43f1808 | ||
|
|
2f6b95cccf | ||
|
|
ad9f6b2b64 | ||
|
|
165c3dc976 | ||
|
|
cbe6390b41 | ||
|
|
026eaf6606 | ||
|
|
ee184f8e62 | ||
|
|
5745231dc9 | ||
|
|
cbdc3a9861 | ||
|
|
ba5c42ad6d | ||
|
|
b056a731a1 | ||
|
|
b4cd94f2d9 | ||
|
|
3b8cb43f02 | ||
|
|
82cb11c3c4 | ||
|
|
eb7522a90b | ||
|
|
3898201b1e | ||
|
|
bb6ab0c17b | ||
|
|
652c40ab1f | ||
|
|
ca0d6215a9 | ||
|
|
61d155257c | ||
|
|
91399f1a1f | ||
|
|
2e738c5977 | ||
|
|
c0bd51cf03 | ||
|
|
ca5afe04f8 | ||
|
|
47db0b9f4e | ||
|
|
27f17a377a | ||
|
|
ecaf731ecf | ||
|
|
9fba2a85f6 | ||
|
|
93f75be182 | ||
|
|
d537901f59 | ||
|
|
260c1ed4a4 | ||
|
|
ac2bd52463 | ||
|
|
16c6f8fee1 | ||
|
|
8a0db21f17 | ||
|
|
9ab08a71c0 | ||
|
|
381c32a3b4 | ||
|
|
350c0f10c4 | ||
|
|
d5b982f813 | ||
|
|
6587375ff2 | ||
|
|
f5ecb81bad | ||
|
|
0141cd81c4 | ||
|
|
ff412c7497 | ||
|
|
623f239f91 | ||
|
|
c7ba304c6a | ||
|
|
c8393604d1 | ||
|
|
c5d47a67aa | ||
|
|
3a1423cd59 | ||
|
|
d57f4cf6cd | ||
|
|
af420942c4 | ||
|
|
eac4e5e66f | ||
|
|
b8822a98f7 | ||
|
|
95322b42c0 | ||
|
|
33f5a06d32 | ||
|
|
a7e5126c92 | ||
|
|
c62f3e5b8e | ||
|
|
5d3f413a05 | ||
|
|
644154ca38 | ||
|
|
cccce5ff74 | ||
|
|
aa1fe98188 | ||
|
|
b6a78550d6 | ||
|
|
70b7f2c493 | ||
|
|
ea44e5f209 | ||
|
|
41ef87d9f8 | ||
|
|
c0b4a70aa0 | ||
|
|
955875cb7a | ||
|
|
827e8571aa | ||
|
|
aed08b0f59 | ||
|
|
3ddaf41e27 | ||
|
|
38a9cce6bf | ||
|
|
ae255ef381 | ||
|
|
4316bef01e | ||
|
|
8491c5706d | ||
|
|
7b7fab1f32 | ||
|
|
5a6fccdc2f | ||
|
|
23668be46d | ||
|
|
f92f2c1666 | ||
|
|
895740a3cb | ||
|
|
4c76c3231f | ||
|
|
ca9d3f9559 | ||
|
|
3ca3c5e75c | ||
|
|
c2c0b1dfdc | ||
|
|
c633774487 | ||
|
|
c2d86fc517 | ||
|
|
d7c8d150ca | ||
|
|
6b8a4e65f2 | ||
|
|
482e43dd1f | ||
|
|
1437babc6c | ||
|
|
5ed882943a | ||
|
|
e678b3fb3a | ||
|
|
a5770bc4e4 | ||
|
|
59821bb939 | ||
|
|
b58040be44 | ||
|
|
be3b8ad52e | ||
|
|
0fdaf97461 | ||
|
|
f5f036372f | ||
|
|
3fceb983e6 | ||
|
|
ae6a39196d | ||
|
|
2810ff2753 | ||
|
|
205c0cf8d3 | ||
|
|
d9c801a091 | ||
|
|
0833aa3b30 | ||
|
|
9c90bbc146 | ||
|
|
231b387076 | ||
|
|
3de432652f | ||
|
|
cad714493e | ||
|
|
6a690a6108 | ||
|
|
1b85c52557 | ||
|
|
b37be92ea9 | ||
|
|
f95979f445 | ||
|
|
09e07218ae | ||
|
|
085f5a9451 | ||
|
|
eb4e935554 | ||
|
|
57986cf425 | ||
|
|
7bf14b9acc | ||
|
|
3ba0be34d9 | ||
|
|
0f828a3c75 | ||
|
|
16a768a142 | ||
|
|
f380d20279 | ||
|
|
be908ab36c | ||
|
|
b545454537 | ||
|
|
a5ff8dd1d6 | ||
|
|
59f23e518b | ||
|
|
858b3f0176 | ||
|
|
89b8494ad0 | ||
|
|
64ecd75bd5 | ||
|
|
138eb3393d | ||
|
|
1ad51d2925 | ||
|
|
317e175bca | ||
|
|
0e287aa7d1 | ||
|
|
28018b705b | ||
|
|
46ec72d6be | ||
|
|
82494a87af | ||
|
|
95c19b447a | ||
|
|
9c6eae936a | ||
|
|
77605a9360 | ||
|
|
82880c3f06 | ||
|
|
11143c771b | ||
|
|
0968eaa5a8 | ||
|
|
0687f9d472 | ||
|
|
25f0f5951a | ||
|
|
eec0ce65ad | ||
|
|
be4e100168 | ||
|
|
59e4f1063c | ||
|
|
359e21f02b | ||
|
|
6894bd9c0e | ||
|
|
7040f02823 | ||
|
|
41408de337 | ||
|
|
72e243dce8 | ||
|
|
77d4840e2a | ||
|
|
0d5961821c | ||
|
|
f34980b173 | ||
|
|
5b21a6e529 | ||
|
|
6dbb737d4b | ||
|
|
76ecfbaaa2 | ||
|
|
d9aacbcb92 | ||
|
|
e088325d95 | ||
|
|
b29dd2c580 | ||
|
|
89e82fd5b0 | ||
|
|
9f0b671d42 | ||
|
|
7de7c724dc | ||
|
|
3525b1d92a | ||
|
|
30cea84886 | ||
|
|
0c2cc7f575 |
@@ -1,4 +1,3 @@
|
||||
UPDATE_PORT=5300
|
||||
PUBLIC_PORT=5400
|
||||
CRON_PORT=5500
|
||||
BACK_PORT=5600
|
||||
@@ -11,4 +10,3 @@ SECRET='whyour'
|
||||
QINIU_AK=''
|
||||
QINIU_SK=''
|
||||
QINIU_SCOPE=''
|
||||
TEMP=''
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: "\U0001F41E Bug report"
|
||||
description: Create a report to help us improve
|
||||
body:
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Qinglong version
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
|
||||
placeholder: Steps to reproduce
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What is expected?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actually-happening
|
||||
attributes:
|
||||
label: What is actually happening?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Info
|
||||
description: Output of `npx envinfo --system --binaries --browsers`
|
||||
render: shell
|
||||
placeholder: System, Binaries, Browsers
|
||||
- type: textarea
|
||||
id: additional-comments
|
||||
attributes:
|
||||
label: Any additional comments?
|
||||
description: e.g. some background/context of how you ran into this bug.
|
||||
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Telegram Chat
|
||||
url: https://t.me/jiao_long
|
||||
about: Ask questions and discuss with other Qinglong users in real time.
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
|
||||
about: Use GitHub discussions for message-board style questions and discussions.
|
||||
@@ -1,40 +0,0 @@
|
||||
name: "\U0001F680 New feature proposal"
|
||||
description: Suggest an idea for this project
|
||||
labels: [":sparkles: feature request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for your interest in the project and taking the time to fill out this feature report!
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Clear and concise description of the problem
|
||||
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: suggested-solution
|
||||
attributes:
|
||||
label: Suggested solution
|
||||
description: "In module [xy] we could provide following implementation..."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternative
|
||||
attributes:
|
||||
label: Alternative
|
||||
description: Clear and concise description of any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Any other context or screenshots about the feature request here.
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
|
||||
required: true
|
||||
@@ -1,31 +0,0 @@
|
||||
## PR Type
|
||||
What kind of change does this PR introduce?
|
||||
|
||||
<!-- Please check the one that applies to this PR using "x". -->
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Feature
|
||||
- [ ] Code style update (formatting, local variables)
|
||||
- [ ] Refactoring (no functional changes, no api changes)
|
||||
- [ ] Other... Please describe:
|
||||
|
||||
|
||||
## What is the current behavior?
|
||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||
|
||||
Issue Number: N/A
|
||||
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
|
||||
## Does this PR introduce a breaking change?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
|
||||
|
||||
## Other information
|
||||
@@ -1,7 +0,0 @@
|
||||
# Comment to be posted to on PRs from first time contributors in your repository
|
||||
newPRWelcomeComment: |
|
||||
💖 Thanks for opening this pull request! 💖
|
||||
Please be patient and we will get back to you as soon as we can.
|
||||
# Comment to be posted to on pull requests merged by a first time user
|
||||
firstPRMergeComment: >
|
||||
Congrats on merging your first pull request! 🎉🎉🎉
|
||||
@@ -2,56 +2,31 @@ name: Build And Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
branches:
|
||||
- "master"
|
||||
- "develop"
|
||||
- "test-cli"
|
||||
- 'master'
|
||||
- 'develop'
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- "v*"
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
schedule:
|
||||
- cron: "00 20 * * *"
|
||||
# 参考 https://jasonet.co/posts/scheduled-actions/
|
||||
# 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:
|
||||
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: "8.3.1"
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "pnpm"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
- name: build front and back
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
npm i -g pnpm
|
||||
pnpm install
|
||||
pnpm build:front
|
||||
pnpm build:back
|
||||
|
||||
@@ -59,9 +34,6 @@ jobs:
|
||||
env:
|
||||
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
||||
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: |
|
||||
mkdir -p tmp
|
||||
cd ./tmp
|
||||
@@ -72,50 +44,34 @@ jobs:
|
||||
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}
|
||||
|
||||
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:
|
||||
needs: build-static
|
||||
|
||||
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用
|
||||
runs-on: ubuntu-20.04
|
||||
# runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
- name: Setup timezone
|
||||
uses: szenius/set-timezone@v1.2
|
||||
- name: Set time zone
|
||||
uses: szenius/set-timezone@v1.0
|
||||
with:
|
||||
timezoneLinux: Asia/Shanghai
|
||||
timezoneLinux: "Asia/Shanghai"
|
||||
timezoneMacos: "Asia/Shanghai"
|
||||
timezoneWindows: "China Standard Time"
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -123,114 +79,38 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# nightly, master, pr-2, 1.2.3, 1.2, 1
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=schedule,pattern=nightly
|
||||
type=edge
|
||||
type=ref,event=branch
|
||||
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={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v3
|
||||
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
|
||||
# platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
context: docker/
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
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@v1.2
|
||||
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@v5
|
||||
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 }}
|
||||
|
||||
+8
-1
@@ -5,6 +5,7 @@
|
||||
/npm-debug.log*
|
||||
/yarn-error.log
|
||||
/yarn.lock
|
||||
/pnpm-lock.yaml
|
||||
/package-lock.json
|
||||
|
||||
# production
|
||||
@@ -22,4 +23,10 @@
|
||||
.env
|
||||
.history
|
||||
.version.ts
|
||||
/.tmp
|
||||
|
||||
/config
|
||||
/log
|
||||
/db
|
||||
/manual_log
|
||||
/scripts
|
||||
/bak
|
||||
@@ -1 +1,2 @@
|
||||
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
|
||||
strict-peer-dependencies=false
|
||||
@@ -6,20 +6,3 @@ package.json
|
||||
.umi
|
||||
.umi-production
|
||||
.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,39 +1,32 @@
|
||||
import { defineConfig } from '@umijs/max';
|
||||
import { defineConfig } from 'umi';
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
|
||||
const baseUrl = process.env.QlBaseUrl || '/';
|
||||
export default defineConfig({
|
||||
hash: true,
|
||||
jsMinifier: 'terser',
|
||||
antd: {},
|
||||
locale: {
|
||||
antd: true,
|
||||
title: true,
|
||||
baseNavigator: true,
|
||||
},
|
||||
layout: false,
|
||||
outputPath: 'static/dist',
|
||||
fastRefresh: true,
|
||||
favicons: [`https://qn.whyour.cn/favicon.svg`],
|
||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||
proxy: {
|
||||
[`${baseUrl}api/update`]: {
|
||||
target: 'http://127.0.0.1:5300/',
|
||||
changeOrigin: true,
|
||||
pathRewrite: { [`^${baseUrl}api/update`]: '/api' },
|
||||
nodeModulesTransform: {
|
||||
type: 'none',
|
||||
},
|
||||
[`${baseUrl}api/public`]: {
|
||||
fastRefresh: {},
|
||||
esbuild: {},
|
||||
webpack5: {},
|
||||
dynamicImport: {
|
||||
loading: '@/components/pageLoading',
|
||||
},
|
||||
favicon: '/images/g5.ico',
|
||||
proxy: {
|
||||
'/api/public': {
|
||||
target: 'http://127.0.0.1:5400/',
|
||||
changeOrigin: true,
|
||||
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
|
||||
},
|
||||
[`${baseUrl}api`]: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:5600/',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
pathRewrite: { [`^${baseUrl}api`]: '/api' },
|
||||
},
|
||||
},
|
||||
chainWebpack: ((config: any) => {
|
||||
chainWebpack: (config) => {
|
||||
config.plugin('compression-webpack-plugin').use(
|
||||
new CompressionPlugin({
|
||||
algorithm: 'gzip',
|
||||
@@ -42,20 +35,13 @@ export default defineConfig({
|
||||
minRatio: 0.6,
|
||||
}),
|
||||
);
|
||||
}) as any,
|
||||
},
|
||||
externals: {
|
||||
react: 'window.React',
|
||||
'react-dom': 'window.ReactDOM',
|
||||
},
|
||||
headScripts: [
|
||||
`./api/env.js`,
|
||||
'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',
|
||||
],
|
||||
copy: [
|
||||
{
|
||||
from: 'node_modules/monaco-editor/min/vs',
|
||||
to: 'static/dist/monaco-editor/min/vs',
|
||||
},
|
||||
scripts: [
|
||||
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
||||
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
|
||||
],
|
||||
});
|
||||
|
||||
+123
-127
@@ -1,29 +1,30 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/whyour/qinglong">
|
||||
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">Green Dragon</h1>
|
||||
|
||||
<div align="center">
|
||||
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||
|
||||
<h1 align="center">Qinglong</h1>
|
||||
Timed task management panel with python3, javaScript, shell, typescript support
|
||||
|
||||
[简体中文](./README.md) | English
|
||||
[![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-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-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-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
|
||||
[Demo](http://demo.dlww.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.dlww.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>
|
||||
|
||||

|
||||
[](https://whyour.cn)
|
||||
|
||||
[简体中文](./README.md) | English
|
||||
|
||||
## Features
|
||||
|
||||
@@ -35,49 +36,85 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
- Support dark mode
|
||||
- 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
|
||||
|
||||
### Docker (Recommended)
|
||||
### Local Deployment
|
||||
|
||||
```bash
|
||||
# curl -sSL get.docker.com | sh
|
||||
# To be completed
|
||||
```
|
||||
|
||||
### Podman Deployment
|
||||
|
||||
1. podman installation
|
||||
|
||||
```bash
|
||||
https://podman.io/getting-started/installation
|
||||
```
|
||||
|
||||
2. start the container
|
||||
|
||||
```bash
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql:/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 \
|
||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||
-v $PWD/ql:/ql/data \
|
||||
-p 5700:5700 \
|
||||
# Deployment paths are not required, e.g. /test.
|
||||
-e QlBaseUrl="/" \
|
||||
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
||||
-e QlPort="5700" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### Docker-compose (Recommended)
|
||||
### 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
|
||||
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
mkdir qinglong
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||
|
||||
@@ -87,66 +124,9 @@ docker-compose up -d
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Podman (Recommended)
|
||||
## Use
|
||||
|
||||
```bash
|
||||
# https://podman.io/getting-started/installation
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||
-p 5700:5700 \
|
||||
# Deployment paths are not required, e.g. /test.
|
||||
-e QlBaseUrl="/" \
|
||||
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
||||
-e QlPort="5700" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### Npm
|
||||
|
||||
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3/pnpm yourself
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
```
|
||||
|
||||
```bash
|
||||
npm install -g node-pre-gyp pnpm@8.3.1
|
||||
npm install -g @whyour/qinglong
|
||||
qinglong
|
||||
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
|
||||
export QL_DIR=""
|
||||
export QL_DATA_DIR=""
|
||||
# Run again
|
||||
qinglong
|
||||
```
|
||||
|
||||
## Built-in commands
|
||||
|
||||
- task
|
||||
|
||||
```bash
|
||||
# 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>
|
||||
# Set task timeout
|
||||
task -m <max_time> <file_path>
|
||||
# Use -- to split, -- followed by a parameter that is passed to the script, as in the following example, the script receives the parameter -u whyour -p password
|
||||
task <file_path> -- -u whyour -p password
|
||||
```
|
||||
|
||||
- ql
|
||||
1. built-in commands
|
||||
|
||||
```bash
|
||||
# Update and restart Green Dragon
|
||||
@@ -167,33 +147,31 @@ ql check
|
||||
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>
|
||||
```
|
||||
|
||||
| **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 |
|
||||
| extensions | Pull the branch of the repository |
|
||||
| branch | Number of days of logs to be kept |
|
||||
| days | File path for task execution |
|
||||
| file_path | The name of the environment variable that needs to be concurrent or specified at the time of task execution |
|
||||
2. parameter description
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
Open your browser and visit <http://127.0.0.1:5700>
|
||||
* 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
|
||||
|
||||
@@ -201,10 +179,28 @@ Open your browser and visit <http://127.0.0.1:5700>
|
||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
- [Ant Design](https://ant.design)
|
||||
- [Ant Design Pro](https://pro.ant.design/)
|
||||
- [Umijs](https://umijs.org)
|
||||
- [Umijs3.0](https://umijs.org)
|
||||
- [darkreader](https://github.com/darkreader/darkreader)
|
||||
- [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
|
||||
|
||||
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".
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
<div align="center">
|
||||
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||
<p align="center">
|
||||
<a href="https://github.com/whyour/qinglong">
|
||||
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">青龙</h1>
|
||||
|
||||
简体中文 | [English](./README-en.md)
|
||||
<div align="center">
|
||||
|
||||
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台
|
||||
支持python3、javaScript、shell、typescript 的定时任务管理面板
|
||||
|
||||
Timed task management platform supporting 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]
|
||||
|
||||
[![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-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-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-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
|
||||
[Demo](http://demo.dlww.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.dlww.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>
|
||||
|
||||

|
||||
[](https://whyour.cn)
|
||||
|
||||
简体中文 | [English](./README-en.md)
|
||||
|
||||
## 功能
|
||||
|
||||
@@ -37,49 +36,86 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
- 支持暗黑模式
|
||||
- 支持手机端操作
|
||||
|
||||
## 版本
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
### docker (推荐)
|
||||
### 本机部署
|
||||
|
||||
```bash
|
||||
# curl -sSL get.docker.com | sh
|
||||
# 待完善
|
||||
```
|
||||
|
||||
### podman 部署
|
||||
|
||||
1. podman 安装
|
||||
|
||||
```bash
|
||||
https://podman.io/getting-started/installation
|
||||
```
|
||||
|
||||
2. 启动容器
|
||||
|
||||
```bash
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql:/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 \
|
||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||
-v $PWD/ql:/ql/data \
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,比如 /test
|
||||
-e QlBaseUrl="/" \
|
||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||
-e QlPort="5700" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### docker-compose (推荐)
|
||||
### 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
|
||||
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
mkdir qinglong
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||
|
||||
@@ -89,64 +125,9 @@ docker-compose up -d
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### podman (推荐)
|
||||
## 使用
|
||||
|
||||
```bash
|
||||
# https://podman.io/getting-started/installation
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,比如 /test
|
||||
-e QlBaseUrl="/" \
|
||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||
-e QlPort="5700" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### npm
|
||||
|
||||
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3/pnpm
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
```
|
||||
|
||||
```bash
|
||||
npm install -g node-pre-gyp pnpm@8.3.1
|
||||
npm install -g @whyour/qinglong
|
||||
qinglong
|
||||
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
|
||||
export QL_DIR=""
|
||||
export QL_DATA_DIR=""
|
||||
# 再次执行
|
||||
qinglong
|
||||
```
|
||||
|
||||
## 内置命令
|
||||
|
||||
- task
|
||||
|
||||
```bash
|
||||
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||
task <file_path>
|
||||
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||
task <file_path> now
|
||||
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
||||
task <file_path> conc <env_name> <account_number>(可选的)
|
||||
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
||||
task <file_path> desi <env_name> <account_number>
|
||||
# 设置任务超时时间
|
||||
task -m <max_time> <file_path>
|
||||
# 使用 -- 分割,-- 后面的参数会传给脚本,下面的例子,脚本就可接收到参数 -u whyour -p password
|
||||
task <file_path> -- -u whyour -p password
|
||||
```
|
||||
|
||||
- ql
|
||||
1. 内置命令
|
||||
|
||||
```bash
|
||||
# 更新并重启青龙
|
||||
@@ -167,33 +148,30 @@ 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>
|
||||
```
|
||||
|
||||
| **参数** | **说明** |
|
||||
|------------|---------------------------------------------------------------------------------------------|
|
||||
| file_url | 脚本地址 |
|
||||
| repo_url | 仓库地址 |
|
||||
| whitelist | 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割 |
|
||||
| blacklist | 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割 |
|
||||
| dependence | 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割 |
|
||||
| extensions | 拉取仓库的文件后缀,多个竖线分割 |
|
||||
| branch | 拉取仓库的分支 |
|
||||
| days | 需要保留的日志的天数 |
|
||||
| file_path | 任务执行时的文件路径 |
|
||||
2. 参数说明
|
||||
|
||||
## 开发
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
打开你的浏览器,访问 <http://127.0.0.1:5700>
|
||||
* file_url: 脚本地址
|
||||
* repo_url: 仓库地址
|
||||
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
|
||||
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
|
||||
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
|
||||
* branch: 拉取仓库的分支
|
||||
* extensions: 拉取仓库的文件后缀
|
||||
* days: 需要保留的日志的天数
|
||||
* file_path: 任务执行时的文件路径
|
||||
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||
|
||||
## 链接
|
||||
|
||||
@@ -201,10 +179,28 @@ pnpm start
|
||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
- [Ant Design](https://ant.design)
|
||||
- [Ant Design Pro](https://pro.ant.design/)
|
||||
- [Umijs](https://umijs.org)
|
||||
- [Umijs3.0](https://umijs.org)
|
||||
- [darkreader](https://github.com/darkreader/darkreader)
|
||||
- [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)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
||||
|
||||
+20
-39
@@ -3,36 +3,19 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fs from 'fs';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import { join } from 'path';
|
||||
import { SAMPLE_FILES } from '../config/const';
|
||||
import ConfigService from '../services/config';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
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(
|
||||
'/files',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const fileList = await fs.readdir(config.configPath, 'utf-8');
|
||||
const fileList = fs.readdirSync(config.configPath, 'utf-8');
|
||||
res.send({
|
||||
code: 200,
|
||||
data: fileList
|
||||
@@ -48,11 +31,24 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/detail',
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const configService = Container.get(ConfigService);
|
||||
await configService.getFile(req.query.path as string, res);
|
||||
let content = '';
|
||||
if (config.blackFileList.includes(req.params.file)) {
|
||||
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) {
|
||||
return next(e);
|
||||
}
|
||||
@@ -74,27 +70,12 @@ export default (app: Router) => {
|
||||
if (config.blackFileList.includes(name)) {
|
||||
res.send({ code: 403, message: '文件无法访问' });
|
||||
}
|
||||
let path = join(config.configPath, name);
|
||||
if (name.startsWith('data/scripts/')) {
|
||||
path = join(config.rootPath, name);
|
||||
}
|
||||
await fs.writeFile(path, content);
|
||||
const path = `${config.configPath}${name}`;
|
||||
fs.writeFileSync(path, content);
|
||||
res.send({ code: 200, message: '保存成功' });
|
||||
} catch (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);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+4
-157
@@ -2,7 +2,6 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import CronService from '../services/cron';
|
||||
import CronViewService from '../services/cronView';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import cron_parser from 'cron-parser';
|
||||
const route = Router();
|
||||
@@ -10,141 +9,11 @@ const route = Router();
|
||||
export default (app: Router) => {
|
||||
app.use('/crons', route);
|
||||
|
||||
route.get(
|
||||
'/views',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.list();
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/views',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
sorts: Joi.array().optional().allow(null),
|
||||
filters: Joi.array().optional(),
|
||||
filterRelation: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/views',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
sorts: Joi.array().optional().allow(null),
|
||||
filters: Joi.array().optional(),
|
||||
filterRelation: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
if (req.body.type === 1) {
|
||||
return res.send({ code: 400, message: '参数错误' });
|
||||
} else {
|
||||
const data = await cronViewService.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
}
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/views',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.remove(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/views/move',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
fromIndex: Joi.number().required(),
|
||||
toIndex: Joi.number().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.move(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/views/disable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.disabled(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/views/enable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.enabled(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.crontabs(req.query as any);
|
||||
const data = await cronService.crontabs(req.query.searchValue as string);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -152,21 +21,6 @@ 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(
|
||||
'/',
|
||||
celebrate({
|
||||
@@ -175,10 +29,6 @@ export default (app: Router) => {
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string().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) => {
|
||||
@@ -335,10 +185,6 @@ export default (app: Router) => {
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
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(),
|
||||
}),
|
||||
}),
|
||||
@@ -458,12 +304,13 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.status({
|
||||
...req.body,
|
||||
status: req.body.status ? parseInt(req.body.status) : undefined,
|
||||
pid: req.body.pid ? parseInt(req.body.pid) : undefined,
|
||||
status: parseInt(req.body.status),
|
||||
pid: parseInt(req.body.pid) || '',
|
||||
});
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
|
||||
@@ -134,20 +134,4 @@ 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+2
-53
@@ -3,22 +3,8 @@ import { Container } from 'typedi';
|
||||
import EnvService from '../services/env';
|
||||
import { Logger } from 'winston';
|
||||
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 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) => {
|
||||
app.use('/envs', route);
|
||||
|
||||
@@ -40,9 +26,7 @@ export default (app: Router) => {
|
||||
body: Joi.array().items(
|
||||
Joi.object({
|
||||
value: Joi.string().required(),
|
||||
name: Joi.string()
|
||||
.required()
|
||||
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
|
||||
name: Joi.string().required(),
|
||||
remarks: Joi.string().optional().allow(''),
|
||||
}),
|
||||
),
|
||||
@@ -51,9 +35,6 @@ export default (app: Router) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
if (!req.body?.length) {
|
||||
return res.send({ code: 400, message: '参数不正确' });
|
||||
}
|
||||
const data = await envService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
@@ -113,6 +94,7 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.move(req.params.id, req.body);
|
||||
@@ -195,37 +177,4 @@ 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+4
-49
@@ -3,9 +3,8 @@ import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import config from '../config';
|
||||
import { getFileContentByName, readDirs, rmPath } from '../config/util';
|
||||
import { getFileContentByName, readDirs } from '../config/util';
|
||||
import { join } from 'path';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
const route = Router();
|
||||
const blacklist = ['.tmp'];
|
||||
|
||||
@@ -15,7 +14,7 @@ export default (app: Router) => {
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const result = await readDirs(config.logPath, config.logPath, blacklist);
|
||||
const result = readDirs(config.logPath, config.logPath, blacklist);
|
||||
res.send({
|
||||
code: 200,
|
||||
data: result,
|
||||
@@ -26,29 +25,10 @@ export default (app: Router) => {
|
||||
}
|
||||
});
|
||||
|
||||
route.get(
|
||||
'/detail',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (blacklist.includes(req.path)) {
|
||||
return res.send({ code: 403, message: '暂无权限' });
|
||||
}
|
||||
const filePath = join(
|
||||
config.logPath,
|
||||
(req.query.path || '') as string,
|
||||
req.query.file as string,
|
||||
);
|
||||
const content = await getFileContentByName(filePath);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
if (blacklist.includes(req.path)) {
|
||||
return res.send({ code: 403, message: '暂无权限' });
|
||||
@@ -58,36 +38,11 @@ export default (app: Router) => {
|
||||
(req.query.path || '') as string,
|
||||
req.params.file,
|
||||
);
|
||||
const content = await getFileContentByName(filePath);
|
||||
const content = getFileContentByName(filePath);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
type: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let { filename, path, type } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.logPath, path, filename);
|
||||
await rmPath(filePath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+27
-103
@@ -4,13 +4,12 @@ import {
|
||||
readDirs,
|
||||
getLastModifyFilePath,
|
||||
readDir,
|
||||
rmPath,
|
||||
} from '../config/util';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fs from 'fs';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import path, { join, parse } from 'path';
|
||||
import ScriptService from '../services/script';
|
||||
@@ -34,33 +33,15 @@ export default (app: Router) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let result = [];
|
||||
const blacklist = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'.pnpm',
|
||||
'pnpm-lock.yaml',
|
||||
'yarn.lock',
|
||||
'package-lock.json',
|
||||
];
|
||||
const blacklist = ['node_modules', '.git'];
|
||||
if (req.query.path) {
|
||||
const targetPath = path.join(
|
||||
config.scriptPath,
|
||||
req.query.path as string,
|
||||
);
|
||||
result = await readDir(targetPath, config.scriptPath, blacklist);
|
||||
result = readDir(targetPath, config.scriptPath, blacklist);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
},
|
||||
);
|
||||
result = readDirs(config.scriptPath, config.scriptPath, blacklist);
|
||||
}
|
||||
res.send({
|
||||
code: 200,
|
||||
@@ -72,31 +53,17 @@ 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(
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const content = await scriptService.getFile(
|
||||
const filePath = join(
|
||||
config.scriptPath,
|
||||
req.query.path as string,
|
||||
req.params.file,
|
||||
);
|
||||
const content = getFileContentByName(filePath);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -110,13 +77,11 @@ export default (app: Router) => {
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, content, originFilename, directory } =
|
||||
req.body as {
|
||||
let { filename, path, content, originFilename } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
content: string;
|
||||
originFilename: string;
|
||||
directory: string;
|
||||
};
|
||||
|
||||
if (!path) {
|
||||
@@ -126,7 +91,7 @@ export default (app: Router) => {
|
||||
path += '/';
|
||||
}
|
||||
if (!path.startsWith('/')) {
|
||||
path = join(config.scriptPath, path);
|
||||
path = `${config.scriptPath}${path}`;
|
||||
}
|
||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||
return res.send({
|
||||
@@ -136,34 +101,25 @@ export default (app: Router) => {
|
||||
}
|
||||
|
||||
if (req.file) {
|
||||
await fs.rename(req.file.path, join(path, filename));
|
||||
return res.send({ code: 200 });
|
||||
}
|
||||
|
||||
if (directory) {
|
||||
await fs.mkdir(join(path, directory), { recursive: true });
|
||||
fs.renameSync(req.file.path, join(path, req.file.filename));
|
||||
return res.send({ code: 200 });
|
||||
}
|
||||
|
||||
if (!originFilename) {
|
||||
originFilename = filename;
|
||||
}
|
||||
const originFilePath = join(
|
||||
path,
|
||||
`${originFilename.replace(/\//g, '')}`,
|
||||
);
|
||||
const filePath = join(path, `${filename.replace(/\//g, '')}`);
|
||||
const fileExists = await fileExist(filePath);
|
||||
if (fileExists) {
|
||||
await fs.copyFile(
|
||||
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
|
||||
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
||||
if (fs.existsSync(originFilePath)) {
|
||||
fs.copyFileSync(
|
||||
originFilePath,
|
||||
join(config.bakPath, originFilename.replace(/\//g, '')),
|
||||
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
|
||||
);
|
||||
if (filename !== originFilename) {
|
||||
await rmPath(originFilePath);
|
||||
fs.unlinkSync(originFilePath);
|
||||
}
|
||||
}
|
||||
await fs.writeFile(filePath, content);
|
||||
fs.writeFileSync(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -189,7 +145,7 @@ export default (app: Router) => {
|
||||
path: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
await fs.writeFile(filePath, content);
|
||||
fs.writeFileSync(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -203,19 +159,17 @@ export default (app: Router) => {
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
type: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, type } = req.body as {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
await rmPath(filePath);
|
||||
fs.unlinkSync(filePath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -236,7 +190,7 @@ export default (app: Router) => {
|
||||
let { filename } = req.body as {
|
||||
filename: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, filename);
|
||||
const filePath = `${config.scriptPath}${filename}`;
|
||||
// const stats = fs.statSync(filePath);
|
||||
// res.set({
|
||||
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
||||
@@ -268,7 +222,7 @@ export default (app: Router) => {
|
||||
let { filename, content, path } = req.body;
|
||||
const { name, ext } = parse(filename);
|
||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||
await fs.writeFile(filePath, content || '', { encoding: 'utf8' });
|
||||
fs.writeFileSync(filePath, content || '', { encoding: 'utf8' });
|
||||
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.runScript(filePath);
|
||||
@@ -284,53 +238,23 @@ export default (app: Router) => {
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
content: Joi.string().optional().allow(''),
|
||||
path: Joi.string().optional().allow(''),
|
||||
pid: Joi.number().optional().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, pid } = req.body;
|
||||
let { filename, content, path } = req.body;
|
||||
const { name, ext } = parse(filename);
|
||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||
const logPath = join(config.logPath, path, `${name}.swap`);
|
||||
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.stopScript(filePath, pid);
|
||||
setTimeout(() => {
|
||||
rmPath(logPath);
|
||||
}, 3000);
|
||||
const result = await scriptService.stopScript(filePath);
|
||||
res.send(result);
|
||||
} catch (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);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,9 +49,6 @@ export default (app: Router) => {
|
||||
sub_after: Joi.string().optional().allow('').allow(null),
|
||||
schedule_type: 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) => {
|
||||
@@ -180,9 +177,6 @@ export default (app: Router) => {
|
||||
sub_before: Joi.string().optional().allow('').allow(null),
|
||||
sub_after: Joi.string().optional().allow('').allow(null),
|
||||
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(),
|
||||
}),
|
||||
}),
|
||||
@@ -210,16 +204,12 @@ export default (app: Router) => {
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
query: Joi.object({
|
||||
force: Joi.boolean().optional(),
|
||||
t: Joi.number()
|
||||
})
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const data = await subscriptionService.remove(req.body, req.query);
|
||||
const data = await subscriptionService.remove(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
||||
+17
-238
@@ -1,30 +1,13 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fs from 'fs';
|
||||
import config from '../config';
|
||||
import SystemService from '../services/system';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import UserService from '../services/user';
|
||||
import {
|
||||
getUniqPath,
|
||||
handleLogPath,
|
||||
parseVersion,
|
||||
promiseExec,
|
||||
} from '../config/util';
|
||||
import dayjs from 'dayjs';
|
||||
import multer from 'multer';
|
||||
|
||||
import { EnvModel } from '../data/env';
|
||||
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) => {
|
||||
app.use('/system', route);
|
||||
@@ -34,14 +17,18 @@ export default (app: Router) => {
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const { version, changeLog, changeLogLink, publishTime } =
|
||||
await parseVersion(config.versionFile);
|
||||
const envCount = await EnvModel.count();
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const version = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
Object.keys(authInfo).length === 2 &&
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'admin'
|
||||
authInfo.password === 'admin' &&
|
||||
envCount === 0
|
||||
) {
|
||||
isInitialized = false;
|
||||
}
|
||||
@@ -50,10 +37,6 @@ export default (app: Router) => {
|
||||
data: {
|
||||
isInitialized,
|
||||
version,
|
||||
publishTime: dayjs(publishTime).unix(),
|
||||
branch: process.env.QL_BRANCH || 'master',
|
||||
changeLog,
|
||||
changeLogLink,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -63,12 +46,12 @@ export default (app: Router) => {
|
||||
});
|
||||
|
||||
route.get(
|
||||
'/config',
|
||||
'/log/remove',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const data = await systemService.getSystemConfig();
|
||||
const data = await systemService.getLogRemoveFrequency();
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -77,16 +60,19 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/log-remove-frequency',
|
||||
'/log/remove',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
logRemoveFrequency: Joi.number().allow(null),
|
||||
frequency: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateLogRemoveFrequency(req.body);
|
||||
const result = await systemService.updateLogRemoveFrequency(
|
||||
req.body.frequency,
|
||||
);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -94,96 +80,6 @@ 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(
|
||||
'/update-check',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -212,25 +108,6 @@ 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(
|
||||
'/notify',
|
||||
celebrate({
|
||||
@@ -250,102 +127,4 @@ 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', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
await systemService.getSystemLog(res);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { celebrate, Joi } from 'celebrate';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import config from '../config';
|
||||
const route = Router();
|
||||
|
||||
@@ -27,10 +26,6 @@ export default (app: Router) => {
|
||||
|
||||
route.post(
|
||||
'/login',
|
||||
rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 100,
|
||||
}),
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
username: Joi.string().required(),
|
||||
@@ -74,9 +69,6 @@ export default (app: Router) => {
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
if (process.env.DeployEnv === 'demo') {
|
||||
return res.send({ code: 450, message: '未知错误' });
|
||||
}
|
||||
const userService = Container.get(UserService);
|
||||
await userService.updateUsernameAndPassword(req.body);
|
||||
res.send({ code: 200, message: '更新成功' });
|
||||
|
||||
+5
-5
@@ -1,6 +1,9 @@
|
||||
import 'reflect-metadata'; // We need this in order to use @Decorators
|
||||
|
||||
import config from './config';
|
||||
|
||||
import express from 'express';
|
||||
|
||||
import Logger from './loaders/logger';
|
||||
|
||||
async function startServer() {
|
||||
@@ -15,14 +18,11 @@ async function startServer() {
|
||||
await require('./loaders/app').default({ expressApp: app });
|
||||
|
||||
const server = app
|
||||
.listen(config.port, '0.0.0.0', () => {
|
||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
||||
console.debug(`✌️ 后端服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
.listen(config.port, () => {
|
||||
Logger.debug(`✌️ Back server launched on port ${config.port}`);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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',
|
||||
},
|
||||
];
|
||||
+6
-49
@@ -1,32 +1,14 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { createRandomString } from './share';
|
||||
import { createRandomString } from './util';
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
if (!process.env.QL_DIR) {
|
||||
// 声明QL_DIR环境变量
|
||||
let qlHomePath = path.join(__dirname, '../../');
|
||||
// 生产环境
|
||||
if (qlHomePath.endsWith('/static/')) {
|
||||
qlHomePath = path.join(qlHomePath, '../');
|
||||
}
|
||||
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 envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||
|
||||
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 tmpPath = path.join(rootPath, '.tmp/');
|
||||
const envFound = dotenv.config();
|
||||
const rootPath = process.cwd();
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
@@ -34,12 +16,8 @@ const bakPath = path.join(dataPath, 'bak/');
|
||||
const logPath = path.join(dataPath, 'log/');
|
||||
const dbPath = path.join(dataPath, 'db/');
|
||||
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 jsEnvFile = path.join(configPath, 'env.js');
|
||||
const pyEnvFile = path.join(configPath, 'env.py');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const crontabFile = path.join(configPath, 'crontab.list');
|
||||
const authConfigFile = path.join(configPath, 'auth.json');
|
||||
@@ -51,10 +29,7 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
|
||||
const authError = '错误的用户名密码,请重试';
|
||||
const loginFaild = '请先登录!';
|
||||
const configString = 'config sample crontab shareCode diy';
|
||||
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');
|
||||
const versionFile = path.join(rootPath, 'src/version.ts');
|
||||
|
||||
if (envFound.error) {
|
||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||
@@ -64,7 +39,6 @@ export default {
|
||||
port: parseInt(process.env.BACK_PORT as string, 10),
|
||||
cronPort: parseInt(process.env.CRON_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),
|
||||
logs: {
|
||||
level: process.env.LOG_LEVEL || 'silly',
|
||||
@@ -73,11 +47,6 @@ export default {
|
||||
prefix: '/api',
|
||||
},
|
||||
rootPath,
|
||||
tmpPath,
|
||||
dataPath,
|
||||
dataTgzFile,
|
||||
shareShellFile,
|
||||
dependenceProxyFile,
|
||||
configString,
|
||||
loginFaild,
|
||||
authError,
|
||||
@@ -89,8 +58,6 @@ export default {
|
||||
sampleFile,
|
||||
confFile,
|
||||
envFile,
|
||||
jsEnvFile,
|
||||
pyEnvFile,
|
||||
dbPath,
|
||||
uploadPath,
|
||||
configPath,
|
||||
@@ -101,10 +68,7 @@ export default {
|
||||
'config.sh.sample',
|
||||
'cookie.sh',
|
||||
'crontab.list',
|
||||
'dependence-proxy.sh',
|
||||
'env.sh',
|
||||
'env.js',
|
||||
'env.py',
|
||||
'token.json',
|
||||
],
|
||||
writePathList: [configPath, scriptPath],
|
||||
@@ -116,15 +80,8 @@ export default {
|
||||
'/api/system',
|
||||
'/api/user/init',
|
||||
'/api/user/notification/init',
|
||||
'/open/user/login',
|
||||
'/open/user/two-factor/login',
|
||||
'/open/system',
|
||||
'/open/user/init',
|
||||
'/open/user/notification/init',
|
||||
],
|
||||
versionFile,
|
||||
lastVersionFile,
|
||||
sqliteFile,
|
||||
sshdPath,
|
||||
systemLogPath,
|
||||
};
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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']);
|
||||
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'),
|
||||
);
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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('');
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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;
|
||||
}
|
||||
+171
-341
@@ -1,36 +1,25 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import got from 'got';
|
||||
import iconv from 'iconv-lite';
|
||||
import { exec } from 'child_process';
|
||||
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';
|
||||
|
||||
export * from './share';
|
||||
|
||||
export async function getFileContentByName(fileName: string) {
|
||||
const _exsit = await fileExist(fileName);
|
||||
if (_exsit) {
|
||||
return await fs.readFile(fileName, 'utf8');
|
||||
export function getFileContentByName(fileName: string) {
|
||||
if (fs.existsSync(fileName)) {
|
||||
return fs.readFileSync(fileName, 'utf8');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function getLastModifyFilePath(dir: string) {
|
||||
export function getLastModifyFilePath(dir: string) {
|
||||
let filePath = '';
|
||||
|
||||
const _exsit = await fileExist(dir);
|
||||
if (_exsit) {
|
||||
const arr = await fs.readdir(dir);
|
||||
if (fs.existsSync(dir)) {
|
||||
const arr = fs.readdirSync(dir);
|
||||
|
||||
arr.forEach(async (item) => {
|
||||
arr.forEach((item) => {
|
||||
const fullpath = path.join(dir, item);
|
||||
const stats = await fs.lstat(fullpath);
|
||||
const stats = fs.statSync(fullpath);
|
||||
if (stats.isFile()) {
|
||||
if (stats.mtimeMs >= 0) {
|
||||
filePath = fullpath;
|
||||
@@ -41,6 +30,91 @@ export async function getLastModifyFilePath(dir: string) {
|
||||
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) {
|
||||
const { authorization = '' } = req.headers;
|
||||
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
||||
@@ -61,8 +135,7 @@ export async function getNetIp(req: any) {
|
||||
...req.ips,
|
||||
req.socket.remoteAddress,
|
||||
]),
|
||||
].filter(Boolean);
|
||||
|
||||
];
|
||||
let ip = ipArray[0];
|
||||
|
||||
if (ipArray.length > 1) {
|
||||
@@ -83,42 +156,34 @@ export async function getNetIp(req: any) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
|
||||
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
||||
ip = '';
|
||||
}
|
||||
|
||||
if (!ip) {
|
||||
return { address: `获取失败`, ip };
|
||||
}
|
||||
|
||||
try {
|
||||
const csdnApi = got
|
||||
.get(`https://searchplugin.csdn.net/api/v1/ip/get?ip=${ip}`, {
|
||||
timeout: 10000,
|
||||
retry: 0,
|
||||
})
|
||||
const baiduApi = got
|
||||
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
||||
.text();
|
||||
const pconlineApi = got
|
||||
const ipApi = got
|
||||
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
|
||||
timeout: 10000,
|
||||
retry: 0,
|
||||
})
|
||||
.buffer();
|
||||
const [csdnBody, pconlineBody] = await await Promise.all<any>([
|
||||
csdnApi,
|
||||
pconlineApi,
|
||||
]);
|
||||
const csdnRes = JSON.parse(csdnBody);
|
||||
const pconlineRes = JSON.parse(iconv.decode(pconlineBody, 'GBK'));
|
||||
let address = '';
|
||||
if (csdnBody && csdnRes.code == 200) {
|
||||
address = csdnRes.data.address;
|
||||
} else if (pconlineRes && pconlineRes.addr) {
|
||||
address = pconlineRes.addr;
|
||||
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
|
||||
|
||||
const ipRegx = /.*IP :(.*)\n/;
|
||||
const addrRegx = /.*数据二 :(.*)\n/;
|
||||
if (data && ipRegx.test(data) && addrRegx.test(data)) {
|
||||
const ip = data.match(ipRegx)[1];
|
||||
const addr = data.match(addrRegx)[1];
|
||||
return { address: addr, ip };
|
||||
} else if (ipApiBody) {
|
||||
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
|
||||
return { address: `${addr}`, ip };
|
||||
} else {
|
||||
return { address: `获取失败`, ip };
|
||||
}
|
||||
return { address, ip };
|
||||
} catch (error) {
|
||||
return { address: `获取失败`, ip };
|
||||
}
|
||||
@@ -158,29 +223,22 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
||||
}
|
||||
|
||||
export async function fileExist(file: any) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
await fs.access(file);
|
||||
return true;
|
||||
fs.accessSync(file);
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
return false;
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function createFile(file: string, data: string = '') {
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
await fs.writeFile(file, data);
|
||||
}
|
||||
|
||||
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;
|
||||
return new Promise((resolve) => {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, data);
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function concurrentRun(
|
||||
@@ -225,82 +283,56 @@ enum FileType {
|
||||
'file',
|
||||
}
|
||||
|
||||
interface IFile {
|
||||
title: string;
|
||||
key: string;
|
||||
type: 'directory' | 'file';
|
||||
parent: string;
|
||||
mtime: number;
|
||||
size?: number;
|
||||
children?: IFile[];
|
||||
}
|
||||
|
||||
export function dirSort(a: IFile, b: IFile): number {
|
||||
if (a.type === 'file' && b.type === 'file') {
|
||||
return b.mtime - a.mtime;
|
||||
} 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,
|
||||
mtime: stats.mtime.getTime(),
|
||||
children: children.sort(sort),
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
title: file,
|
||||
type: 'file',
|
||||
key,
|
||||
parent: relativePath,
|
||||
size: stats.size,
|
||||
mtime: stats.mtime.getTime(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result.sort(sort);
|
||||
}
|
||||
|
||||
export async function readDir(
|
||||
export function readDirs(
|
||||
dir: string,
|
||||
baseDir: string = '',
|
||||
blacklist: string[] = [],
|
||||
) {
|
||||
const relativePath = path.relative(baseDir, dir);
|
||||
const files = await fs.readdir(dir);
|
||||
const files = fs.readdirSync(dir);
|
||||
const result: any = files
|
||||
.filter((x) => !blacklist.includes(x))
|
||||
.map(async (file: string) => {
|
||||
.map((file: string) => {
|
||||
const subPath = path.join(dir, file);
|
||||
const stats = await fs.lstat(subPath);
|
||||
const stats = fs.statSync(subPath);
|
||||
const key = path.join(relativePath, file);
|
||||
if (stats.isDirectory()) {
|
||||
return {
|
||||
title: file,
|
||||
key,
|
||||
type: 'directory',
|
||||
disabled: true,
|
||||
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',
|
||||
key,
|
||||
parent: relativePath,
|
||||
};
|
||||
});
|
||||
return result.sort(
|
||||
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
|
||||
);
|
||||
}
|
||||
|
||||
export function readDir(
|
||||
dir: string,
|
||||
baseDir: string = '',
|
||||
blacklist: string[] = [],
|
||||
) {
|
||||
const relativePath = path.relative(baseDir, dir);
|
||||
const files = fs.readdirSync(dir);
|
||||
const result: any = files
|
||||
.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);
|
||||
return {
|
||||
title: file,
|
||||
@@ -312,216 +344,14 @@ export async function readDir(
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function promiseExec(command: string): Promise<string> {
|
||||
try {
|
||||
const { stderr, stdout } = await promisify(exec)(command, {
|
||||
maxBuffer: 200 * 1024 * 1024,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return stdout || stderr;
|
||||
} catch (error) {
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function promiseExecSuccess(command: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await promisify(exec)(command, {
|
||||
maxBuffer: 200 * 1024 * 1024,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return stdout || '';
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function parseHeaders(headers: string) {
|
||||
if (!headers) return {};
|
||||
|
||||
const parsed: any = {};
|
||||
let key;
|
||||
let val;
|
||||
let i;
|
||||
|
||||
headers &&
|
||||
headers.split('\n').forEach(function parser(line) {
|
||||
i = line.indexOf(':');
|
||||
key = line.substring(0, i).trim().toLowerCase();
|
||||
val = line.substring(i + 1).trim();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
|
||||
});
|
||||
|
||||
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(
|
||||
body: string,
|
||||
contentType:
|
||||
| 'application/json'
|
||||
| 'multipart/form-data'
|
||||
| 'application/x-www-form-urlencoded'
|
||||
| 'text/plain',
|
||||
valueFormatFn?: (v: string) => string,
|
||||
) {
|
||||
if (contentType === 'text/plain' || !body) {
|
||||
return valueFormatFn && body ? valueFormatFn(body) : body;
|
||||
}
|
||||
|
||||
const parsed = parseString(body, valueFormatFn);
|
||||
|
||||
switch (contentType) {
|
||||
case 'multipart/form-data':
|
||||
return Object.keys(parsed).reduce((p, c) => {
|
||||
p.append(c, parsed[c]);
|
||||
return p;
|
||||
}, new FormData());
|
||||
case 'application/x-www-form-urlencoded':
|
||||
return Object.keys(parsed).reduce((p, c) => {
|
||||
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
|
||||
});
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function psTree(pid: number): Promise<number[]> {
|
||||
export function promiseExec(command: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
psTreeFun(pid, (err: any, pids: number[]) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(pids.filter((x) => !isNaN(x)));
|
||||
});
|
||||
exec(
|
||||
command,
|
||||
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
|
||||
(err, stdout, stderr) => {
|
||||
resolve(stdout || stderr || JSON.stringify(err));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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 async function parseContentVersion(content: string): Promise<IVersion> {
|
||||
return load(content) as IVersion;
|
||||
}
|
||||
|
||||
export async function getUniqPath(
|
||||
command: string,
|
||||
id: string,
|
||||
): Promise<string> {
|
||||
if (/^\d+$/.test(id)) {
|
||||
id = `_${id}`;
|
||||
} else {
|
||||
id = '';
|
||||
}
|
||||
|
||||
const items = command.split(/ +/);
|
||||
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}${id}`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
+6
-18
@@ -17,19 +17,15 @@ export class Crontab {
|
||||
labels?: string[];
|
||||
last_running_time?: number;
|
||||
last_execution_time?: number;
|
||||
sub_id?: number;
|
||||
extra_schedules?: Array<{ schedule: string }>;
|
||||
task_before?: string;
|
||||
task_after?: string;
|
||||
|
||||
constructor(options: Crontab) {
|
||||
this.name = options.name;
|
||||
this.command = options.command.trim();
|
||||
this.command = options.command;
|
||||
this.schedule = options.schedule;
|
||||
this.saved = options.saved;
|
||||
this.id = options.id;
|
||||
this.status =
|
||||
typeof options.status === 'number' && CrontabStatus[options.status]
|
||||
options.status && CrontabStatus[options.status]
|
||||
? options.status
|
||||
: CrontabStatus.idle;
|
||||
this.timestamp = new Date().toString();
|
||||
@@ -41,21 +37,17 @@ export class Crontab {
|
||||
this.labels = options.labels || [];
|
||||
this.last_running_time = options.last_running_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 {
|
||||
'running' = 0,
|
||||
'queued' = 0.5,
|
||||
'idle' = 1,
|
||||
'running',
|
||||
'idle',
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
|
||||
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { }
|
||||
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
||||
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||
name: {
|
||||
unique: 'compositeIndex',
|
||||
@@ -80,8 +72,4 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||
labels: DataTypes.JSON,
|
||||
last_running_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,
|
||||
});
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { sequelize } from '.';
|
||||
import { DataTypes, Model } from 'sequelize';
|
||||
|
||||
export enum CronViewType {
|
||||
'系统' = 1,
|
||||
'个人',
|
||||
}
|
||||
|
||||
interface SortType {
|
||||
type: 'ASC' | 'DESC';
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface FilterType {
|
||||
property: string;
|
||||
operation: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class CrontabView {
|
||||
name?: string;
|
||||
id?: number;
|
||||
position?: number;
|
||||
isDisabled?: 1 | 0;
|
||||
filters?: FilterType[];
|
||||
sorts?: SortType[];
|
||||
filterRelation?: 'and' | 'or';
|
||||
type?: CronViewType;
|
||||
|
||||
constructor(options: CrontabView) {
|
||||
this.name = options.name;
|
||||
this.id = options.id;
|
||||
this.position = options.position;
|
||||
this.isDisabled = options.isDisabled || 0;
|
||||
this.filters = options.filters;
|
||||
this.sorts = options.sorts;
|
||||
this.filterRelation = options.filterRelation;
|
||||
this.type = options.type || CronViewType.个人;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CronViewInstance
|
||||
extends Model<CrontabView, CrontabView>,
|
||||
CrontabView {}
|
||||
export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
||||
'CrontabView',
|
||||
{
|
||||
name: {
|
||||
unique: 'name',
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
position: DataTypes.NUMBER,
|
||||
isDisabled: DataTypes.NUMBER,
|
||||
filters: DataTypes.JSON,
|
||||
sorts: DataTypes.JSON,
|
||||
filterRelation: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
type: DataTypes.NUMBER,
|
||||
},
|
||||
);
|
||||
+9
-26
@@ -4,21 +4,18 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
export class Dependence {
|
||||
timestamp?: string;
|
||||
id?: number;
|
||||
status: DependenceStatus;
|
||||
type: DependenceTypes;
|
||||
name: string;
|
||||
status?: DependenceStatus;
|
||||
type?: DependenceTypes;
|
||||
name?: number;
|
||||
log?: string[];
|
||||
remark?: string;
|
||||
|
||||
constructor(options: Dependence) {
|
||||
this.id = options.id;
|
||||
this.status =
|
||||
typeof options.status === 'number' && DependenceStatus[options.status]
|
||||
? options.status
|
||||
: DependenceStatus.queued;
|
||||
this.status = options.status || DependenceStatus.installing;
|
||||
this.type = options.type || DependenceTypes.nodejs;
|
||||
this.timestamp = new Date().toString();
|
||||
this.name = options.name.trim();
|
||||
this.name = options.name;
|
||||
this.log = options.log || [];
|
||||
this.remark = options.remark || '';
|
||||
}
|
||||
@@ -31,8 +28,6 @@ export enum DependenceStatus {
|
||||
'removing',
|
||||
'removed',
|
||||
'removeFailed',
|
||||
'queued',
|
||||
'cancelled',
|
||||
}
|
||||
|
||||
export enum DependenceTypes {
|
||||
@@ -43,29 +38,17 @@ export enum DependenceTypes {
|
||||
|
||||
export enum InstallDependenceCommandTypes {
|
||||
'pnpm add -g',
|
||||
'pip3 install --disable-pip-version-check --root-user-action=ignore',
|
||||
'apk add --no-check-certificate',
|
||||
}
|
||||
|
||||
export enum GetDependenceCommandTypes {
|
||||
'pnpm ls -g ',
|
||||
'pip3 show --disable-pip-version-check',
|
||||
'apk info -es',
|
||||
}
|
||||
|
||||
export enum versionDependenceCommandTypes {
|
||||
'@',
|
||||
'==',
|
||||
'=',
|
||||
'pip3 install',
|
||||
'apk add',
|
||||
}
|
||||
|
||||
export enum unInstallDependenceCommandTypes {
|
||||
'pnpm remove -g',
|
||||
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
||||
'pip3 uninstall -y',
|
||||
'apk del',
|
||||
}
|
||||
|
||||
export interface DependenceInstance
|
||||
interface DependenceInstance
|
||||
extends Model<Dependence, Dependence>,
|
||||
Dependence {}
|
||||
export const DependenceModel = sequelize.define<DependenceInstance>(
|
||||
|
||||
+3
-9
@@ -13,10 +13,7 @@ export class Env {
|
||||
constructor(options: Env) {
|
||||
this.value = options.value;
|
||||
this.id = options.id;
|
||||
this.status =
|
||||
typeof options.status === 'number' && EnvStatus[options.status]
|
||||
? options.status
|
||||
: EnvStatus.normal;
|
||||
this.status = options.status || EnvStatus.normal;
|
||||
this.timestamp = new Date().toString();
|
||||
this.position = options.position;
|
||||
this.name = options.name;
|
||||
@@ -29,12 +26,9 @@ export enum EnvStatus {
|
||||
'disabled',
|
||||
}
|
||||
|
||||
export const maxPosition = 9000000000000000;
|
||||
export const initPosition = 4500000000000000;
|
||||
export const stepPosition = 10000000000;
|
||||
export const minPosition = 100;
|
||||
export const initEnvPosition = 9999999999;
|
||||
|
||||
export interface EnvInstance extends Model<Env, Env>, Env {}
|
||||
interface EnvInstance extends Model<Env, Env>, Env {}
|
||||
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||
timestamp: DataTypes.STRING,
|
||||
|
||||
+4
-12
@@ -1,23 +1,15 @@
|
||||
import { Sequelize, Transaction } from 'sequelize';
|
||||
import { Sequelize } from 'sequelize';
|
||||
import config from '../config/index';
|
||||
import { join } from 'path';
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: join(config.dbPath, 'database.sqlite'),
|
||||
storage: `${config.dbPath}database.sqlite`,
|
||||
logging: false,
|
||||
retry: {
|
||||
max: 10,
|
||||
match: ['SQLITE_BUSY: database is locked'],
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 2,
|
||||
max: 6,
|
||||
min: 0,
|
||||
idle: 30000,
|
||||
acquire: 30000,
|
||||
evict: 10000,
|
||||
},
|
||||
transactionType: Transaction.TYPES.IMMEDIATE,
|
||||
});
|
||||
|
||||
export type ResponseType<T> = { code: number; data?: T; message?: string };
|
||||
|
||||
+3
-69
@@ -1,25 +1,16 @@
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
|
||||
export enum NotificationMode {
|
||||
'gotify' = 'gotify',
|
||||
'goCqHttpBot' = 'goCqHttpBot',
|
||||
'serverChan' = 'serverChan',
|
||||
'pushDeer' = 'pushDeer',
|
||||
'bark' = 'bark',
|
||||
'chat' = 'chat',
|
||||
'telegramBot' = 'telegramBot',
|
||||
'dingtalkBot' = 'dingtalkBot',
|
||||
'weWorkBot' = 'weWorkBot',
|
||||
'weWorkApp' = 'weWorkApp',
|
||||
'aibotk' = 'aibotk',
|
||||
'iGot' = 'iGot',
|
||||
'pushPlus' = 'pushPlus',
|
||||
'wePlusBot' = 'wePlusBot',
|
||||
'email' = 'email',
|
||||
'pushMe' = 'pushMe',
|
||||
'feishu' = 'feishu',
|
||||
'webhook' = 'webhook',
|
||||
'chronocat' = 'Chronocat',
|
||||
}
|
||||
|
||||
abstract class NotificationBaseInfo {
|
||||
@@ -44,22 +35,13 @@ export class ServerChanNotification extends NotificationBaseInfo {
|
||||
|
||||
export class PushDeerNotification extends NotificationBaseInfo {
|
||||
public pushDeerKey = '';
|
||||
public pushDeerUrl = '';
|
||||
}
|
||||
|
||||
export class ChatNotification extends NotificationBaseInfo {
|
||||
public chatUrl = '';
|
||||
public chatToken = '';
|
||||
}
|
||||
|
||||
export class BarkNotification extends NotificationBaseInfo {
|
||||
public barkPush = '';
|
||||
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
||||
public barkIcon = 'http://qn.whyour.cn/logo.png';
|
||||
public barkSound = '';
|
||||
public barkGroup = 'qinglong';
|
||||
public barkLevel = 'active';
|
||||
public barkUrl = '';
|
||||
public barkArchive=""
|
||||
}
|
||||
|
||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||
@@ -68,7 +50,7 @@ export class TelegramBotNotification extends NotificationBaseInfo {
|
||||
public telegramBotProxyHost = '';
|
||||
public telegramBotProxyPort = '';
|
||||
public telegramBotProxyAuth = '';
|
||||
public telegramBotApiHost = 'https://api.telegram.org';
|
||||
public telegramBotApiHost = 'api.telegram.org';
|
||||
}
|
||||
|
||||
export class DingtalkBotNotification extends NotificationBaseInfo {
|
||||
@@ -78,18 +60,10 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
|
||||
|
||||
export class WeWorkBotNotification extends NotificationBaseInfo {
|
||||
public weWorkBotKey = '';
|
||||
public weWorkOrigin = '';
|
||||
}
|
||||
|
||||
export class WeWorkAppNotification extends NotificationBaseInfo {
|
||||
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 {
|
||||
@@ -101,62 +75,22 @@ export class PushPlusNotification extends NotificationBaseInfo {
|
||||
public pushPlusUser = '';
|
||||
}
|
||||
|
||||
export class WePlusBotNotification extends NotificationBaseInfo {
|
||||
public wePlusBotToken = '';
|
||||
public wePlusBotReceiver = '';
|
||||
public wePlusBotVersion = '';
|
||||
}
|
||||
|
||||
export class EmailNotification extends NotificationBaseInfo {
|
||||
public emailService: string = '';
|
||||
public emailUser: 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 {
|
||||
public webhookHeaders: string = '';
|
||||
public webhookBody: string = '';
|
||||
public webhookUrl: string = '';
|
||||
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
|
||||
public webhookContentType:
|
||||
| 'application/json'
|
||||
| 'multipart/form-data'
|
||||
| 'application/x-www-form-urlencoded' = 'application/json';
|
||||
}
|
||||
|
||||
export class LarkNotification extends NotificationBaseInfo {
|
||||
public larkKey = '';
|
||||
}
|
||||
|
||||
export interface NotificationInfo
|
||||
extends GoCqHttpBotNotification,
|
||||
GotifyNotification,
|
||||
ServerChanNotification,
|
||||
PushDeerNotification,
|
||||
ChatNotification,
|
||||
BarkNotification,
|
||||
TelegramBotNotification,
|
||||
DingtalkBotNotification,
|
||||
WeWorkBotNotification,
|
||||
WeWorkAppNotification,
|
||||
AibotkNotification,
|
||||
IGotNotification,
|
||||
PushPlusNotification,
|
||||
WePlusBotNotification,
|
||||
EmailNotification,
|
||||
PushMeNotification,
|
||||
WebhookNotification,
|
||||
ChronocatNotification,
|
||||
LarkNotification {}
|
||||
|
||||
EmailNotification {}
|
||||
|
||||
+8
-1
@@ -26,7 +26,14 @@ export interface AppToken {
|
||||
|
||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
||||
|
||||
export interface AppInstance extends Model<App, App>, App {}
|
||||
export enum CrontabStatus {
|
||||
'running',
|
||||
'idle',
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
|
||||
interface AppInstance extends Model<App, App>, App {}
|
||||
export const AppModel = sequelize.define<AppInstance>('App', {
|
||||
name: { type: DataTypes.STRING, unique: 'name' },
|
||||
scopes: DataTypes.JSON,
|
||||
|
||||
+1
-4
@@ -16,7 +16,4 @@ export type SockMessageType =
|
||||
| 'uninstallDependence'
|
||||
| 'updateSystemVersion'
|
||||
| 'manuallyRunScript'
|
||||
| 'runSubscriptionEnd'
|
||||
| 'reloadSystem'
|
||||
| 'updateNodeMirror'
|
||||
| 'updateLinuxMirror';
|
||||
| 'runSubscriptionEnd';
|
||||
|
||||
@@ -28,17 +28,14 @@ export class Subscription {
|
||||
extensions?: string;
|
||||
sub_before?: string;
|
||||
sub_after?: string;
|
||||
proxy?: string;
|
||||
autoAddCron?: 1 | 0;
|
||||
autoDelCron?: 1 | 0;
|
||||
|
||||
constructor(options: Subscription) {
|
||||
this.id = options.id;
|
||||
this.name = options.name || options.alias;
|
||||
this.type = options.type;
|
||||
this.schedule = options.schedule;
|
||||
this.status = this.status =
|
||||
typeof options.status === 'number' && SubscriptionStatus[options.status]
|
||||
this.status =
|
||||
options.status && SubscriptionStatus[options.status]
|
||||
? options.status
|
||||
: SubscriptionStatus.idle;
|
||||
this.url = options.url;
|
||||
@@ -57,9 +54,6 @@ export class Subscription {
|
||||
this.extensions = options.extensions;
|
||||
this.sub_before = options.sub_before;
|
||||
this.sub_after = options.sub_after;
|
||||
this.proxy = options.proxy;
|
||||
this.autoAddCron = options.autoAddCron ? 1 : 0;
|
||||
this.autoDelCron = options.autoDelCron ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +64,7 @@ export enum SubscriptionStatus {
|
||||
'queued',
|
||||
}
|
||||
|
||||
export interface SubscriptionInstance
|
||||
interface SubscriptionInstance
|
||||
extends Model<Subscription, Subscription>,
|
||||
Subscription {}
|
||||
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
||||
@@ -108,8 +102,5 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
||||
log_path: DataTypes.STRING,
|
||||
schedule_type: DataTypes.STRING,
|
||||
alias: { type: DataTypes.STRING, unique: 'alias' },
|
||||
proxy: { type: DataTypes.STRING, allowNull: true },
|
||||
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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',
|
||||
}
|
||||
|
||||
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 type SystemModelInfo = SystemConfigInfo &
|
||||
Partial<NotificationInfo> &
|
||||
LoginLogInfo;
|
||||
|
||||
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,5 +3,3 @@ declare namespace Express {
|
||||
platform: 'desktop' | 'mobile';
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'pstree.remy';
|
||||
|
||||
+8
-13
@@ -7,23 +7,18 @@ import linkDeps from './deps';
|
||||
import initTask from './initTask';
|
||||
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
depInjectorLoader();
|
||||
Logger.info('✌️ Dependency loaded');
|
||||
console.log('✌️ Dependency loaded');
|
||||
await depInjectorLoader();
|
||||
Logger.info('✌️ Dependency Injector loaded');
|
||||
|
||||
await expressLoader({ app: expressApp });
|
||||
Logger.info('✌️ Express loaded');
|
||||
|
||||
await initData();
|
||||
Logger.info('✌️ Init data loaded');
|
||||
console.log('✌️ Init data loaded');
|
||||
Logger.info('✌️ init data loaded');
|
||||
|
||||
await linkDeps();
|
||||
Logger.info('✌️ Link deps loaded');
|
||||
console.log('✌️ Link deps loaded');
|
||||
Logger.info('✌️ link deps loaded');
|
||||
|
||||
initTask();
|
||||
Logger.info('✌️ Init task loaded');
|
||||
console.log('✌️ Init task loaded');
|
||||
|
||||
expressLoader({ app: expressApp });
|
||||
Logger.info('✌️ Express loaded');
|
||||
console.log('✌️ Express loaded');
|
||||
Logger.info('✌️ init task loaded');
|
||||
};
|
||||
|
||||
+13
-46
@@ -5,59 +5,27 @@ import { EnvModel } from '../data/env';
|
||||
import { CrontabModel } from '../data/cron';
|
||||
import { DependenceModel } from '../data/dependence';
|
||||
import { AppModel } from '../data/open';
|
||||
import { SystemModel } from '../data/system';
|
||||
import { AuthModel } from '../data/auth';
|
||||
import { sequelize } from '../data';
|
||||
import { fileExist } from '../config/util';
|
||||
import { SubscriptionModel } from '../data/subscription';
|
||||
import { CrontabViewModel } from '../data/cronView';
|
||||
import config from '../config';
|
||||
import { sequelize } from '../data';
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
await CrontabModel.sync();
|
||||
await DependenceModel.sync();
|
||||
await AppModel.sync();
|
||||
await SystemModel.sync();
|
||||
await AuthModel.sync();
|
||||
await EnvModel.sync();
|
||||
await SubscriptionModel.sync();
|
||||
await CrontabViewModel.sync();
|
||||
|
||||
// 初始化新增字段
|
||||
try {
|
||||
await sequelize.query(
|
||||
'alter table CrontabViews add column filterRelation VARCHAR(255)',
|
||||
);
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query(
|
||||
'alter table Subscriptions add column proxy VARCHAR(255)',
|
||||
);
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query('alter table CrontabViews add column type NUMBER');
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query(
|
||||
'alter table Subscriptions add column autoAddCron NUMBER',
|
||||
);
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query(
|
||||
'alter table Subscriptions add column autoDelCron NUMBER',
|
||||
);
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
|
||||
} catch (error) { }
|
||||
try {
|
||||
await sequelize.query('alter table Crontabs add column extra_schedules JSON');
|
||||
} catch (error) { }
|
||||
try {
|
||||
await sequelize.query('alter table Crontabs add column task_before TEXT');
|
||||
} catch (error) { }
|
||||
try {
|
||||
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
||||
} catch (error) { }
|
||||
// try {
|
||||
// const queryInterface = sequelize.getQueryInterface();
|
||||
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
|
||||
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
|
||||
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
|
||||
// } catch (error) { }
|
||||
|
||||
// 2.10-2.11 升级
|
||||
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
||||
@@ -75,7 +43,7 @@ export default async () => {
|
||||
const dependenceCount = await DependenceModel.count();
|
||||
const envCount = await EnvModel.count();
|
||||
const appCount = await AppModel.count();
|
||||
const authCount = await SystemModel.count();
|
||||
const authCount = await AuthModel.count();
|
||||
if (crondbExist && cronCount === 0) {
|
||||
const cronDb = new DataStore({
|
||||
filename: cronDbFile,
|
||||
@@ -127,14 +95,13 @@ export default async () => {
|
||||
});
|
||||
authDb.persistence.compactDatafile();
|
||||
authDb.find({}).exec(async (err, docs) => {
|
||||
await SystemModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✌️ DB loaded');
|
||||
Logger.info('✌️ DB loaded');
|
||||
} catch (error) {
|
||||
console.error('✌️ DB load failed');
|
||||
Logger.error(error);
|
||||
Logger.info('✌️ DB load failed');
|
||||
Logger.info(error);
|
||||
}
|
||||
};
|
||||
|
||||
+20
-29
@@ -1,58 +1,49 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import chokidar from 'chokidar';
|
||||
import config from '../config/index';
|
||||
import { fileExist, promiseExec, rmPath } from '../config/util';
|
||||
import { promiseExec } from '../config/util';
|
||||
|
||||
async function linkToNodeModule(src: string, dst?: string) {
|
||||
function linkToNodeModule(src: string, dst?: string) {
|
||||
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
||||
const source = path.join(config.rootPath, src);
|
||||
|
||||
try {
|
||||
const stats = await fs.lstat(target);
|
||||
if (!stats) {
|
||||
await fs.symlink(source, target, 'dir');
|
||||
fs.lstat(target, (err, stat) => {
|
||||
if (!stat) {
|
||||
fs.symlink(source, target, 'dir', (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
});
|
||||
}
|
||||
|
||||
async function linkCommand() {
|
||||
const commandPath = await promiseExec('which node');
|
||||
const commandDir = path.dirname(commandPath);
|
||||
const oldLinkShell = [
|
||||
const linkShell = [
|
||||
{
|
||||
src: 'update.sh',
|
||||
dest: 'ql',
|
||||
tmp: 'ql_tmp',
|
||||
},
|
||||
{
|
||||
src: 'task.sh',
|
||||
dest: 'task',
|
||||
},
|
||||
];
|
||||
// const newLinkShell = [
|
||||
// {
|
||||
// src: 'task.mjs',
|
||||
// dest: 'task',
|
||||
// tmp: 'task_tmp',
|
||||
// },
|
||||
// ];
|
||||
|
||||
for (const link of oldLinkShell) {
|
||||
for (const link of linkShell) {
|
||||
const source = path.join(config.rootPath, 'shell', link.src);
|
||||
const target = path.join(commandDir, link.dest);
|
||||
const tmpTarget = path.join(commandDir, link.tmp);
|
||||
await fs.symlink(source, tmpTarget);
|
||||
await fs.rename(tmpTarget, target);
|
||||
if (fs.existsSync(target)) {
|
||||
fs.unlinkSync(target);
|
||||
}
|
||||
fs.symlink(source, target, (err) => {});
|
||||
}
|
||||
// for (const link of newLinkShell) {
|
||||
// const source = path.join(config.rootPath, 'cli', link.src);
|
||||
// const target = path.join(commandDir, link.dest);
|
||||
// const tmpTarget = path.join(commandDir, link.tmp);
|
||||
// await fs.symlink(source, tmpTarget);
|
||||
// await fs.rename(tmpTarget, target);
|
||||
// }
|
||||
}
|
||||
|
||||
export default async (src: string = 'deps') => {
|
||||
await linkCommand();
|
||||
await linkToNodeModule(src);
|
||||
linkToNodeModule(src);
|
||||
|
||||
const source = path.join(config.rootPath, src);
|
||||
const watcher = chokidar.watch(source, {
|
||||
|
||||
+28
-29
@@ -3,42 +3,39 @@ import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import routes from '../api';
|
||||
import config from '../config';
|
||||
import jwt, { UnauthorizedError } from 'express-jwt';
|
||||
import fs from 'fs/promises';
|
||||
import { getPlatform, getToken, safeJSONParse } from '../config/util';
|
||||
import jwt from 'express-jwt';
|
||||
import fs from 'fs';
|
||||
import { getPlatform, getToken } from '../config/util';
|
||||
import Container from 'typedi';
|
||||
import OpenService from '../services/open';
|
||||
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 { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
import { serveEnv } from '../config/serverEnv';
|
||||
import Logger from './logger';
|
||||
|
||||
export default ({ app }: { app: Application }) => {
|
||||
app.set('trust proxy', 'loopback');
|
||||
app.enable('trust proxy');
|
||||
app.use(cors());
|
||||
app.get(`${config.api.prefix}/env.js`, serveEnv);
|
||||
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
||||
|
||||
app.use(
|
||||
'/api/public',
|
||||
createProxyMiddleware({
|
||||
target: `http://0.0.0.0:${config.publicPort}/api`,
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '/api/public': '' },
|
||||
logProvider: () => Logger,
|
||||
}),
|
||||
);
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
||||
next();
|
||||
} else {
|
||||
return handler(req, res, {
|
||||
public: 'static/dist',
|
||||
rewrites: [{ source: '**', destination: '/index.html' }],
|
||||
});
|
||||
}
|
||||
});
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||
|
||||
app.use(
|
||||
jwt({
|
||||
secret: config.secret,
|
||||
secret: config.secret as string,
|
||||
algorithms: ['HS384'],
|
||||
}).unless({
|
||||
path: [...config.apiWhiteList, /^\/open\//],
|
||||
@@ -78,24 +75,22 @@ export default ({ app }: { app: Application }) => {
|
||||
if (
|
||||
!headerToken &&
|
||||
originPath &&
|
||||
config.apiWhiteList.includes(originPath)
|
||||
config.apiWhiteList.includes(originPath) &&
|
||||
originPath !== '/api/crons/status'
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const data = await fs.readFile(config.authConfigFile, 'utf8');
|
||||
if (data && headerToken) {
|
||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
if (data) {
|
||||
const { token = '', tokens = {} } = JSON.parse(data);
|
||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
|
||||
const errorMessage = headerToken
|
||||
? 'jwt malformed'
|
||||
: 'No authorization token was found';
|
||||
const err = new UnauthorizedError(errorCode, { message: errorMessage });
|
||||
const err: any = new Error('UnauthorizedError');
|
||||
err.status = 401;
|
||||
next(err);
|
||||
});
|
||||
|
||||
@@ -105,12 +100,14 @@ export default ({ app }: { app: Application }) => {
|
||||
}
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const envCount = await EnvModel.count();
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
Object.keys(authInfo).length === 2 &&
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'admin'
|
||||
authInfo.password === 'admin' &&
|
||||
envCount === 0
|
||||
) {
|
||||
isInitialized = false;
|
||||
}
|
||||
@@ -171,6 +168,8 @@ export default ({ app }: { app: Application }) => {
|
||||
},
|
||||
);
|
||||
|
||||
app.use(Sentry.Handlers.errorHandler());
|
||||
|
||||
app.use(
|
||||
(
|
||||
err: Error & { status: number },
|
||||
|
||||
+18
-66
@@ -4,86 +4,38 @@ import { Container } from 'typedi';
|
||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import CronService from '../services/cron';
|
||||
import EnvService from '../services/env';
|
||||
import { DependenceModel, DependenceStatus } from '../data/dependence';
|
||||
import _ from 'lodash';
|
||||
import { DependenceModel } from '../data/dependence';
|
||||
import { Op } from 'sequelize';
|
||||
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';
|
||||
|
||||
export default async () => {
|
||||
const cronService = Container.get(CronService);
|
||||
const envService = Container.get(EnvService);
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const systemService = Container.get(SystemService);
|
||||
|
||||
// 初始化增加系统配置
|
||||
await SystemModel.upsert({ type: AuthDataType.systemConfig });
|
||||
await SystemModel.upsert({ type: AuthDataType.notification });
|
||||
// 初始化更新所有任务状态为空闲
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle },
|
||||
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
|
||||
);
|
||||
|
||||
const installDependencies = () => {
|
||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||
DependenceModel.findAll({
|
||||
where: {},
|
||||
order: [
|
||||
['type', 'DESC'],
|
||||
['createdAt', 'DESC'],
|
||||
],
|
||||
order: [['type', '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 镜像源配置
|
||||
const systemConfig = await systemService.getSystemConfig();
|
||||
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,
|
||||
}).then((docs) => {
|
||||
if (docs.length === 0) {
|
||||
CrontabViewModel.create({
|
||||
name: '全部任务',
|
||||
type: CronViewType.系统,
|
||||
position: initPosition / 2,
|
||||
});
|
||||
const groups = _.groupBy(docs, 'type');
|
||||
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
|
||||
for (const key of keys) {
|
||||
const group = groups[key];
|
||||
const depIds = group.map((x) => x.id);
|
||||
await dependenceService.reInstall(depIds as number[]);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化更新所有任务状态为空闲
|
||||
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
|
||||
|
||||
// 初始化时执行一次所有的 ql repo 任务
|
||||
// 初始化时执行一次所有的ql repo 任务
|
||||
CrontabModel.findAll({
|
||||
where: {
|
||||
isDisabled: { [Op.ne]: 1 },
|
||||
@@ -125,7 +77,7 @@ export default async () => {
|
||||
if (doc.command.includes(`${config.rootPath}/log/`)) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
command: `${config.dataPath}/log/${doc.command.replace(
|
||||
command: `${config.rootPath}/data/log/${doc.command.replace(
|
||||
`${config.rootPath}/log/`,
|
||||
'',
|
||||
)}`,
|
||||
@@ -136,7 +88,7 @@ export default async () => {
|
||||
if (doc.command.includes(`${config.rootPath}/config/`)) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
command: `${config.dataPath}/config/${doc.command.replace(
|
||||
command: `${config.rootPath}/data/config/${doc.command.replace(
|
||||
`${config.rootPath}/config/`,
|
||||
'',
|
||||
)}`,
|
||||
@@ -147,7 +99,7 @@ export default async () => {
|
||||
if (doc.command.includes(`${config.rootPath}/db/`)) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
command: `${config.dataPath}/db/${doc.command.replace(
|
||||
command: `${config.rootPath}/data/db/${doc.command.replace(
|
||||
`${config.rootPath}/db/`,
|
||||
'',
|
||||
)}`,
|
||||
|
||||
+29
-76
@@ -1,38 +1,24 @@
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import dotenv from 'dotenv';
|
||||
import Logger from './logger';
|
||||
import { fileExist } from '../config/util';
|
||||
|
||||
const rootPath = process.env.QL_DIR as string;
|
||||
let dataPath = path.join(rootPath, 'data/');
|
||||
|
||||
if (process.env.QL_DATA_DIR) {
|
||||
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
|
||||
}
|
||||
|
||||
const rootPath = process.cwd();
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
const logPath = path.join(dataPath, 'log/');
|
||||
const uploadPath = path.join(dataPath, 'upload/');
|
||||
const bakPath = path.join(dataPath, 'bak/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const tmpPath = path.join(logPath, '.tmp/');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const authConfigFile = path.join(configPath, 'auth.json');
|
||||
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 TaskBeforeFile = path.join(configPath, 'task_before.sh');
|
||||
const TaskAfterFile = path.join(configPath, 'task_after.sh');
|
||||
const homedir = os.homedir();
|
||||
const sshPath = path.resolve(homedir, '.ssh');
|
||||
const sshdPath = path.join(dataPath, 'ssh.d');
|
||||
const systemLogPath = path.join(dataPath, 'syslog');
|
||||
|
||||
export default async () => {
|
||||
const authFileExist = await fileExist(authConfigFile);
|
||||
@@ -43,81 +29,48 @@ export default async () => {
|
||||
const uploadDirExist = await fileExist(uploadPath);
|
||||
const sshDirExist = await fileExist(sshPath);
|
||||
const bakDirExist = await fileExist(bakPath);
|
||||
const sshdDirExist = await fileExist(sshdPath);
|
||||
const systemLogDirExist = await fileExist(systemLogPath);
|
||||
const tmpDirExist = await fileExist(tmpPath);
|
||||
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
|
||||
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
|
||||
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
|
||||
const TaskAfterFileExist = await fileExist(TaskAfterFile);
|
||||
|
||||
if (!configDirExist) {
|
||||
await fs.mkdir(configPath);
|
||||
fs.mkdirSync(configPath);
|
||||
}
|
||||
|
||||
if (!scriptDirExist) {
|
||||
await fs.mkdir(scriptPath);
|
||||
}
|
||||
|
||||
if (!logDirExist) {
|
||||
await fs.mkdir(logPath);
|
||||
}
|
||||
|
||||
if (!tmpDirExist) {
|
||||
await fs.mkdir(tmpPath);
|
||||
}
|
||||
|
||||
if (!uploadDirExist) {
|
||||
await fs.mkdir(uploadPath);
|
||||
}
|
||||
|
||||
if (!sshDirExist) {
|
||||
await fs.mkdir(sshPath);
|
||||
}
|
||||
|
||||
if (!bakDirExist) {
|
||||
await fs.mkdir(bakPath);
|
||||
}
|
||||
|
||||
if (!sshdDirExist) {
|
||||
await fs.mkdir(sshdPath);
|
||||
}
|
||||
|
||||
if (!systemLogDirExist) {
|
||||
await fs.mkdir(systemLogPath);
|
||||
}
|
||||
|
||||
// 初始化文件
|
||||
if (!authFileExist) {
|
||||
await fs.writeFile(authConfigFile, await fs.readFile(sampleAuthFile));
|
||||
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||
}
|
||||
|
||||
if (!confFileExist) {
|
||||
await fs.writeFile(confFile, await fs.readFile(sampleConfigFile));
|
||||
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
||||
}
|
||||
|
||||
if (!scriptNotifyJsFileExist) {
|
||||
await fs.writeFile(
|
||||
scriptNotifyJsFile,
|
||||
await fs.readFile(sampleNotifyJsFile),
|
||||
);
|
||||
if (!scriptDirExist) {
|
||||
fs.mkdirSync(scriptPath);
|
||||
}
|
||||
|
||||
if (!scriptNotifyPyFileExist) {
|
||||
await fs.writeFile(
|
||||
scriptNotifyPyFile,
|
||||
await fs.readFile(sampleNotifyPyFile),
|
||||
);
|
||||
if (!logDirExist) {
|
||||
fs.mkdirSync(logPath);
|
||||
}
|
||||
|
||||
if (!TaskBeforeFileExist) {
|
||||
await fs.writeFile(TaskBeforeFile, await fs.readFile(sampleTaskShellFile));
|
||||
if (!uploadDirExist) {
|
||||
fs.mkdirSync(uploadPath);
|
||||
}
|
||||
|
||||
if (!TaskAfterFileExist) {
|
||||
await fs.writeFile(TaskAfterFile, await fs.readFile(sampleTaskShellFile));
|
||||
if (!sshDirExist) {
|
||||
fs.mkdirSync(sshPath);
|
||||
}
|
||||
|
||||
if (!bakDirExist) {
|
||||
fs.mkdirSync(bakPath);
|
||||
}
|
||||
|
||||
dotenv.config({ path: confFile });
|
||||
|
||||
// 声明QL_DIR环境变量
|
||||
let qlHomePath = path.join(__dirname, '../../');
|
||||
// 生产环境
|
||||
if (qlHomePath.endsWith('/static/')) {
|
||||
qlHomePath = path.join(qlHomePath, '../');
|
||||
}
|
||||
process.env.QL_DIR = qlHomePath;
|
||||
|
||||
Logger.info('✌️ Init file down');
|
||||
console.log('✌️ Init file down');
|
||||
};
|
||||
|
||||
+16
-31
@@ -1,51 +1,36 @@
|
||||
import { Container } from 'typedi';
|
||||
import _ from 'lodash';
|
||||
import SystemService from '../services/system';
|
||||
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
||||
import ScheduleService from '../services/schedule';
|
||||
import SubscriptionService from '../services/subscription';
|
||||
import config from '../config';
|
||||
import { fileExist } from '../config/util';
|
||||
import { join } from 'path';
|
||||
|
||||
export default async () => {
|
||||
const systemService = Container.get(SystemService);
|
||||
const scheduleService = Container.get(ScheduleService);
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
|
||||
// 生成内置token
|
||||
let tokenCommand = `tsx ${join(config.rootPath, 'back/token.ts')}`;
|
||||
const tokenFile = join(config.rootPath, 'static/build/token.js');
|
||||
|
||||
if (await fileExist(tokenFile)) {
|
||||
tokenCommand = `node ${tokenFile}`;
|
||||
}
|
||||
const cron = {
|
||||
id: NaN,
|
||||
name: '生成token',
|
||||
command: tokenCommand,
|
||||
} as ScheduleTaskType;
|
||||
await scheduleService.cancelIntervalTask(cron);
|
||||
scheduleService.createIntervalTask(cron, {
|
||||
days: 28,
|
||||
});
|
||||
|
||||
// 运行删除日志任务
|
||||
const data = await systemService.getSystemConfig();
|
||||
if (data && data.info && data.info.logRemoveFrequency) {
|
||||
const rmlogCron = {
|
||||
id: data.id as number,
|
||||
const data = await systemService.getLogRemoveFrequency();
|
||||
if (data && data.info && data.info.frequency) {
|
||||
const cron = {
|
||||
id: data.id,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||
command: `ql rmlog ${data.info.frequency}`,
|
||||
};
|
||||
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||
scheduleService.createIntervalTask(rmlogCron, {
|
||||
days: data.info.logRemoveFrequency,
|
||||
await scheduleService.createIntervalTask(cron, {
|
||||
days: data.info.frequency,
|
||||
runImmediately: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 运行所有订阅
|
||||
await subscriptionService.setSshConfig();
|
||||
const subs = await subscriptionService.list();
|
||||
for (const sub of subs) {
|
||||
subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
|
||||
await subscriptionService.handleTask(
|
||||
sub,
|
||||
!sub.is_disabled,
|
||||
true,
|
||||
!sub.is_disabled,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+21
-27
@@ -1,38 +1,32 @@
|
||||
import winston from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
import config from '../config';
|
||||
import path from 'path';
|
||||
|
||||
const levelMap: Record<string, string> = {
|
||||
info: '🔵',
|
||||
warn: '🟡',
|
||||
error: '🔴',
|
||||
debug: '🔶'
|
||||
}
|
||||
|
||||
const customFormat = winston.format.combine(
|
||||
const transports = [];
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
transports.push(new winston.transports.Console());
|
||||
} else {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.cli(),
|
||||
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({
|
||||
level: config.logs.level,
|
||||
levels: winston.config.npm.levels,
|
||||
transports: [
|
||||
new winston.transports.DailyRotateFile({
|
||||
filename: path.join(config.systemLogPath, '%DATE%.log'),
|
||||
...defaultOptions,
|
||||
})
|
||||
],
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
}),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
winston.format.json(),
|
||||
),
|
||||
transports,
|
||||
});
|
||||
|
||||
export default LoggerInstance;
|
||||
|
||||
+6
-17
@@ -1,32 +1,21 @@
|
||||
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';
|
||||
import { parseVersion } from '../config/util';
|
||||
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
const { version } = await parseVersion(config.versionFile);
|
||||
|
||||
export default ({ expressApp }: { expressApp: Application }) => {
|
||||
Sentry.init({
|
||||
ignoreErrors: [
|
||||
/SequelizeUniqueConstraintError/i,
|
||||
/Validation error/i,
|
||||
/UnauthorizedError/i,
|
||||
/celebrate request validation failed/i,
|
||||
],
|
||||
dsn: 'https://8b5c84cfef3e22541bc84de0ed00497b@o1098464.ingest.sentry.io/6122819',
|
||||
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
||||
integrations: [
|
||||
new Sentry.Integrations.Http({ tracing: true }),
|
||||
new Sentry.Integrations.Express({ app: expressApp }),
|
||||
new Tracing.Integrations.Express({ app: expressApp }),
|
||||
],
|
||||
tracesSampleRate: 0.8,
|
||||
release: version,
|
||||
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
expressApp.use(Sentry.Handlers.requestHandler());
|
||||
expressApp.use(Sentry.Handlers.tracingHandler());
|
||||
|
||||
Logger.info('✌️ Sentry loaded');
|
||||
console.log('✌️ Sentry loaded');
|
||||
};
|
||||
|
||||
@@ -6,11 +6,11 @@ export default async ({ server }: { server: Server }) => {
|
||||
await Sock({ server });
|
||||
Logger.info('✌️ Sock loaded');
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
Logger.error('Uncaught exception:', error);
|
||||
process.on('SIGINT', () => {
|
||||
Logger.info('✌️ Server need close');
|
||||
server.close(() => {
|
||||
Logger.info('✌️ Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
Logger.error('Unhandled rejection:', reason, promise);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import sockJs from 'sockjs';
|
||||
import { Server } from 'http';
|
||||
import Logger from './logger';
|
||||
import { Container } from 'typedi';
|
||||
import SockService from '../services/sock';
|
||||
import config from '../config/index';
|
||||
import fs from 'fs/promises';
|
||||
import { getPlatform, safeJSONParse } from '../config/util';
|
||||
import fs from 'fs';
|
||||
import { getPlatform } from '../config/util';
|
||||
|
||||
export default async ({ server }: { server: Server }) => {
|
||||
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||
const sockService = Container.get(SockService);
|
||||
|
||||
echo.on('connection', async (conn) => {
|
||||
echo.on('connection', (conn) => {
|
||||
if (!conn.headers || !conn.url || !conn.pathname) {
|
||||
conn.close('404');
|
||||
}
|
||||
|
||||
const data = await fs.readFile(config.authConfigFile, 'utf8');
|
||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||
if (data) {
|
||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||
const { token = '', tokens = {} } = JSON.parse(data);
|
||||
if (headerToken === token || tokens[platform] === headerToken) {
|
||||
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
||||
sockService.addClient(conn);
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import bodyParser from 'body-parser';
|
||||
import { errors } from 'celebrate';
|
||||
import cors from 'cors';
|
||||
import { Application, NextFunction, Request, Response } from 'express';
|
||||
import jwt 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(
|
||||
jwt({
|
||||
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,
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
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 {}
|
||||
@@ -1,502 +0,0 @@
|
||||
/* eslint-disable */
|
||||
import {
|
||||
CallOptions,
|
||||
ChannelCredentials,
|
||||
Client,
|
||||
ClientOptions,
|
||||
ClientUnaryCall,
|
||||
handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
Metadata,
|
||||
ServiceError,
|
||||
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) ? String(object.schedule) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: ISchedule): unknown {
|
||||
const obj: any = {};
|
||||
message.schedule !== undefined && (obj.schedule = message.schedule);
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
|
||||
return ISchedule.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
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) ? String(object.id) : "",
|
||||
schedule: isSet(object.schedule) ? String(object.schedule) : "",
|
||||
command: isSet(object.command) ? String(object.command) : "",
|
||||
extraSchedules: Array.isArray(object?.extraSchedules)
|
||||
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
|
||||
: [],
|
||||
name: isSet(object.name) ? String(object.name) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: ICron): unknown {
|
||||
const obj: any = {};
|
||||
message.id !== undefined && (obj.id = message.id);
|
||||
message.schedule !== undefined && (obj.schedule = message.schedule);
|
||||
message.command !== undefined && (obj.command = message.command);
|
||||
if (message.extraSchedules) {
|
||||
obj.extraSchedules = message.extraSchedules.map((e) => e ? ISchedule.toJSON(e) : undefined);
|
||||
} else {
|
||||
obj.extraSchedules = [];
|
||||
}
|
||||
message.name !== undefined && (obj.name = message.name);
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
|
||||
return ICron.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
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: Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
|
||||
},
|
||||
|
||||
toJSON(message: AddCronRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.crons) {
|
||||
obj.crons = message.crons.map((e) => e ? ICron.toJSON(e) : undefined);
|
||||
} else {
|
||||
obj.crons = [];
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
|
||||
return AddCronRequest.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
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 ?? {});
|
||||
},
|
||||
|
||||
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: Array.isArray(object?.ids) ? object.ids.map((e: any) => String(e)) : [] };
|
||||
},
|
||||
|
||||
toJSON(message: DeleteCronRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.ids) {
|
||||
obj.ids = message.ids.map((e) => e);
|
||||
} else {
|
||||
obj.ids = [];
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
|
||||
return DeleteCronRequest.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
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 ?? {});
|
||||
},
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Array<infer U> ? 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;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
/* eslint-disable */
|
||||
import {
|
||||
CallOptions,
|
||||
ChannelCredentials,
|
||||
Client,
|
||||
ClientOptions,
|
||||
ClientReadableStream,
|
||||
ClientUnaryCall,
|
||||
handleServerStreamingCall,
|
||||
handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
Metadata,
|
||||
ServiceError,
|
||||
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) ? String(object.service) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: HealthCheckRequest): unknown {
|
||||
const obj: any = {};
|
||||
message.service !== undefined && (obj.service = message.service);
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
|
||||
return HealthCheckRequest.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
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 = {};
|
||||
message.status !== undefined && (obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
|
||||
return HealthCheckResponse.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Array<infer U> ? 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;
|
||||
}
|
||||
+12
-18
@@ -1,36 +1,30 @@
|
||||
import express from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import Logger from './loaders/logger';
|
||||
import config from './config';
|
||||
import { HealthClient } from './protos/health';
|
||||
import { credentials } from '@grpc/grpc-js';
|
||||
|
||||
const app = express();
|
||||
const client = new HealthClient(
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
credentials.createInsecure(),
|
||||
{ 'grpc.enable_http_proxy': 0 },
|
||||
);
|
||||
|
||||
app.get('/api/health', (req, res) => {
|
||||
client.check({ service: 'cron' }, (err, response) => {
|
||||
if (err) {
|
||||
return res.status(200).send({ code: 500, error: err });
|
||||
app.get('/api/public/panel/log', (req, res) => {
|
||||
exec(
|
||||
'pm2 logs panel --lines 500 --nostream --timestamp',
|
||||
(err, stdout, stderr) => {
|
||||
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
|
||||
.listen(config.publicPort, '0.0.0.0', async () => {
|
||||
.listen(config.publicPort, async () => {
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
await require('./loaders/db').default();
|
||||
|
||||
Logger.debug(`✌️ 公共服务启动成功!`);
|
||||
console.debug(`✌️ 公共服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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
|
||||
) {
|
||||
schedule.scheduleJob(task.schedule, function () {
|
||||
let command = task.command as string;
|
||||
if (!command.includes('task ') && !command.includes('ql ')) {
|
||||
command = `task ${command}`;
|
||||
}
|
||||
exec(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.info(`
|
||||
################################################
|
||||
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
||||
################################################
|
||||
`);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
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, { name, schedule, extraSchedules });
|
||||
}),
|
||||
...(extraSchedules?.length
|
||||
? extraSchedules.map((x) =>
|
||||
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||
runCron(command, { name, schedule, extraSchedules });
|
||||
}),
|
||||
)
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
|
||||
callback(null, null);
|
||||
};
|
||||
|
||||
export { addCron };
|
||||
@@ -1,41 +0,0 @@
|
||||
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();
|
||||
@@ -1,6 +0,0 @@
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import { ToadScheduler } from 'toad-scheduler';
|
||||
|
||||
export const scheduleStacks = new Map<string, nodeSchedule.Job[]>();
|
||||
|
||||
export const intervalSchedule = new ToadScheduler();
|
||||
@@ -1,24 +0,0 @@
|
||||
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 };
|
||||
@@ -1,35 +0,0 @@
|
||||
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 };
|
||||
@@ -1,25 +0,0 @@
|
||||
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';
|
||||
|
||||
const server = new Server({ 'grpc.enable_http_proxy': 0 });
|
||||
server.addService(HealthService, { check });
|
||||
server.addService(CronService, { addCron, delCron });
|
||||
server.bindAsync(
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
ServerCredentials.createInsecure(),
|
||||
(err, port) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
server.start();
|
||||
Logger.debug(`✌️ 定时服务启动成功!`);
|
||||
console.debug(`✌️ 定时服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
},
|
||||
);
|
||||
@@ -1,30 +0,0 @@
|
||||
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 = '';
|
||||
if (config.blackFileList.includes(filePath) || !filePath) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
+221
-356
@@ -2,33 +2,23 @@ import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import fs from 'fs/promises';
|
||||
import { exec, execSync, spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import cron_parser from 'cron-parser';
|
||||
import {
|
||||
getFileContentByName,
|
||||
fileExist,
|
||||
killTask,
|
||||
getUniqPath,
|
||||
safeJSONParse,
|
||||
} from '../config/util';
|
||||
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
|
||||
import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { Op } from 'sequelize';
|
||||
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 pickBy from 'lodash/pickBy';
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
private isNodeCron(cron: Crontab) {
|
||||
const { schedule, extra_schedules } = cron;
|
||||
if (Number(schedule?.split(/ +/).length) > 5 || extra_schedules?.length) {
|
||||
private isSixCron(cron: Crontab) {
|
||||
const { schedule } = cron;
|
||||
if (schedule?.split(/ +/).length === 6) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -38,18 +28,7 @@ export default class CronService {
|
||||
const tab = new Crontab(payload);
|
||||
tab.saved = false;
|
||||
const doc = await this.insert(tab);
|
||||
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();
|
||||
await this.set_crontab(this.isSixCron(doc));
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -58,28 +37,9 @@ export default class CronService {
|
||||
}
|
||||
|
||||
public async update(payload: Crontab): Promise<Crontab> {
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new Crontab({ ...doc, ...payload });
|
||||
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();
|
||||
payload.saved = false;
|
||||
const newDoc = await this.updateDb(payload);
|
||||
await this.set_crontab(this.isSixCron(newDoc));
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
@@ -103,7 +63,7 @@ export default class CronService {
|
||||
last_running_time: number;
|
||||
last_execution_time: number;
|
||||
}) {
|
||||
let options: any = {
|
||||
const options: any = {
|
||||
status,
|
||||
pid,
|
||||
log_path,
|
||||
@@ -113,28 +73,12 @@ export default class CronService {
|
||||
options.last_running_time = last_running_time;
|
||||
}
|
||||
|
||||
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 } },
|
||||
);
|
||||
}
|
||||
return await CrontabModel.update({ ...options }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
await CrontabModel.destroy({ where: { id: ids } });
|
||||
await cronClient.delCron(ids.map(String));
|
||||
await this.set_crontab();
|
||||
await this.set_crontab(true);
|
||||
}
|
||||
|
||||
public async pin(ids: number[]) {
|
||||
@@ -169,80 +113,9 @@ export default class CronService {
|
||||
}
|
||||
}
|
||||
|
||||
private formatViewQuery(query: any, viewQuery: any) {
|
||||
if (viewQuery.filters && viewQuery.filters.length > 0) {
|
||||
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
|
||||
if (!query[primaryOperate]) {
|
||||
query[primaryOperate] = [];
|
||||
}
|
||||
for (const col of viewQuery.filters) {
|
||||
const { property, value, operation } = col;
|
||||
let q: any = {};
|
||||
let operate2 = null;
|
||||
let operate = null;
|
||||
switch (operation) {
|
||||
case 'Reg':
|
||||
operate = Op.like;
|
||||
operate2 = Op.or;
|
||||
break;
|
||||
case 'NotReg':
|
||||
operate = Op.notLike;
|
||||
operate2 = Op.and;
|
||||
break;
|
||||
case 'In':
|
||||
q[Op.or] = [
|
||||
{
|
||||
[property]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
property === 'status' && value.includes(2)
|
||||
? { isDisabled: 1 }
|
||||
: {},
|
||||
];
|
||||
break;
|
||||
case 'Nin':
|
||||
q[Op.and] = [
|
||||
{
|
||||
[property]: {
|
||||
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
},
|
||||
property === 'status' && value.includes(2)
|
||||
? { isDisabled: { [Op.ne]: 1 } }
|
||||
: {},
|
||||
];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (operate && operate2) {
|
||||
q[property] = {
|
||||
[Op.or]: [
|
||||
{
|
||||
[operate2]: [
|
||||
{ [operate]: `%${value}%` },
|
||||
{ [operate]: `%${encodeURI(value)}%` },
|
||||
],
|
||||
},
|
||||
{
|
||||
[operate2]: [
|
||||
where(colFn(property), operate, `%${value}%`),
|
||||
where(colFn(property), operate, `%${encodeURI(value)}%`),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
query[primaryOperate].push(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatSearchText(query: any, searchText: string | undefined) {
|
||||
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
||||
let query = {};
|
||||
if (searchText) {
|
||||
if (!query[Op.and]) {
|
||||
query[Op.and] = [];
|
||||
}
|
||||
let q: any = {};
|
||||
const textArray = searchText.split(':');
|
||||
switch (textArray[0]) {
|
||||
case 'name':
|
||||
@@ -250,21 +123,24 @@ export default class CronService {
|
||||
case 'schedule':
|
||||
case 'label':
|
||||
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
|
||||
q[column] = {
|
||||
query = {
|
||||
[column]: {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${textArray[1]}%` },
|
||||
{ [Op.like]: `%${encodeURI(textArray[1])}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
||||
],
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
{ [Op.like]: `%${encodeURI(searchText)}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||
],
|
||||
};
|
||||
q[Op.or] = [
|
||||
query = {
|
||||
[Op.or]: [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
@@ -277,114 +153,25 @@ export default class CronService {
|
||||
{
|
||||
labels: reg,
|
||||
},
|
||||
];
|
||||
],
|
||||
};
|
||||
break;
|
||||
}
|
||||
query[Op.and].push(q);
|
||||
}
|
||||
}
|
||||
|
||||
private formatFilterQuery(query: any, filterQuery: any) {
|
||||
if (filterQuery) {
|
||||
if (!query[Op.and]) {
|
||||
query[Op.and] = [];
|
||||
}
|
||||
const filterKeys: any = Object.keys(filterQuery);
|
||||
for (const key of filterKeys) {
|
||||
let q: any = {};
|
||||
if (!filterQuery[key]) continue;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatViewSort(order: string[][], viewQuery: any) {
|
||||
if (viewQuery.sorts && viewQuery.sorts.length > 0) {
|
||||
for (const { property, type } of viewQuery.sorts) {
|
||||
order.unshift([property, type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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?: {
|
||||
searchValue: string;
|
||||
page: string;
|
||||
size: string;
|
||||
sorter: string;
|
||||
filters: string;
|
||||
queryString: string;
|
||||
}): Promise<{ data: Crontab[]; total: number }> {
|
||||
const searchText = params?.searchValue;
|
||||
const page = Number(params?.page || '0');
|
||||
const size = Number(params?.size || '0');
|
||||
const viewQuery = safeJSONParse(params?.queryString);
|
||||
const filterQuery = safeJSONParse(params?.filters);
|
||||
const sorterQuery = safeJSONParse(params?.sorter);
|
||||
|
||||
let query: any = {};
|
||||
let order = [
|
||||
['isPinned', 'DESC'],
|
||||
['isDisabled', 'ASC'],
|
||||
['status', 'ASC'],
|
||||
['createdAt', 'DESC'],
|
||||
];
|
||||
|
||||
this.formatViewQuery(query, viewQuery);
|
||||
this.formatSearchText(query, searchText);
|
||||
this.formatFilterQuery(query, filterQuery);
|
||||
this.formatViewSort(order, viewQuery);
|
||||
|
||||
if (sorterQuery) {
|
||||
const { field, type } = sorterQuery;
|
||||
if (field && type) {
|
||||
order.unshift([field, type]);
|
||||
}
|
||||
}
|
||||
let condition: any = {
|
||||
const result = await CrontabModel.findAll({
|
||||
where: query,
|
||||
order: order,
|
||||
};
|
||||
if (page && size) {
|
||||
condition.offset = (page - 1) * size;
|
||||
condition.limit = size;
|
||||
}
|
||||
try {
|
||||
const result = await CrontabModel.findAll(condition);
|
||||
const count = await CrontabModel.count({ where: query });
|
||||
return { data: result, total: count };
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
return result as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {
|
||||
public async getDb(query: any): Promise<Crontab> {
|
||||
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
||||
if (!doc) {
|
||||
throw new Error(`Cron ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
return doc && (doc.get({ plain: true }) as Crontab);
|
||||
}
|
||||
|
||||
public async run(ids: number[]) {
|
||||
@@ -392,9 +179,10 @@ export default class CronService {
|
||||
{ status: CrontabStatus.queued },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
ids.forEach((id) => {
|
||||
this.runSingle(id);
|
||||
});
|
||||
concurrentRun(
|
||||
ids.map((id) => async () => await this.runSingle(id)),
|
||||
10,
|
||||
);
|
||||
}
|
||||
|
||||
public async stop(ids: number[]) {
|
||||
@@ -402,11 +190,31 @@ export default class CronService {
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
try {
|
||||
await killTask(doc.pid);
|
||||
process.kill(-doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
this.logger.silly(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(
|
||||
@@ -415,87 +223,112 @@ export default class CronService {
|
||||
);
|
||||
}
|
||||
|
||||
private async runSingle(cronId: number): Promise<number | void> {
|
||||
return taskLimit.manualRunWithCronLimit(() => {
|
||||
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中不存在,所以截取前三个
|
||||
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 });
|
||||
const params = {
|
||||
name: cron.name,
|
||||
command: cron.command,
|
||||
schedule: cron.schedule,
|
||||
extraSchedules: cron.extra_schedules,
|
||||
};
|
||||
if (cron.status !== CrontabStatus.queued) {
|
||||
resolve(params);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`,
|
||||
);
|
||||
|
||||
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 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}`;
|
||||
}
|
||||
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' },
|
||||
);
|
||||
if (cmdStr.endsWith('.js')) {
|
||||
cmdStr = `${cmdStr} now`;
|
||||
}
|
||||
|
||||
const cp = spawn(cmdStr, { shell: '/bin/bash' });
|
||||
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
|
||||
{ status: CrontabStatus.running, pid: cp.pid },
|
||||
{ where: { id } },
|
||||
);
|
||||
cp.stdout.on('data', async (data) => {
|
||||
await fs.appendFile(absolutePath, data.toString());
|
||||
cp.stderr.on('data', (data) => {
|
||||
if (logFileExist) {
|
||||
fs.appendFileSync(`${absolutePath}`, `${data}`);
|
||||
}
|
||||
});
|
||||
cp.stderr.on('data', async (data) => {
|
||||
await fs.appendFile(absolutePath, data.toString());
|
||||
});
|
||||
cp.on('error', async (err) => {
|
||||
await fs.appendFile(absolutePath, JSON.stringify(err));
|
||||
cp.on('error', (err) => {
|
||||
if (logFileExist) {
|
||||
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||
}
|
||||
});
|
||||
|
||||
cp.on('exit', async (code) => {
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||
);
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
resolve({ ...params, pid: cp.pid, code });
|
||||
resolve();
|
||||
});
|
||||
cp.on('close', async (code) => {
|
||||
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||
await cronClient.delCron(ids.map(String));
|
||||
await this.set_crontab();
|
||||
await this.set_crontab(true);
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||
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();
|
||||
await this.set_crontab(true);
|
||||
}
|
||||
|
||||
public async log(id: number) {
|
||||
@@ -507,86 +340,130 @@ export default class CronService {
|
||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||
if (logFileExist) {
|
||||
return await getFileContentByName(`${absolutePath}`);
|
||||
return 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 {
|
||||
return '任务未运行';
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public async logs(id: number) {
|
||||
const doc = await this.getDb({ id });
|
||||
if (!doc || !doc.log_path) {
|
||||
if (!doc) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (doc.log_path) {
|
||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||
const dir = path.resolve(config.logPath, relativeDir);
|
||||
const dirExist = await fileExist(dir);
|
||||
if (dirExist) {
|
||||
let files = await fs.readdir(dir);
|
||||
return (
|
||||
await Promise.all(
|
||||
files.map(async (x) => ({
|
||||
if (existsSync(dir)) {
|
||||
let files = await promises.readdir(dir);
|
||||
return files
|
||||
.map((x) => ({
|
||||
filename: x,
|
||||
directory: relativeDir.replace(config.logPath, ''),
|
||||
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(),
|
||||
})),
|
||||
)
|
||||
).sort((a, b) => b.time - a.time);
|
||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
||||
}))
|
||||
.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 {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private makeCommand(tab: Crontab, realTime?: boolean) {
|
||||
let command = tab.command.trim();
|
||||
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||
command = `${TASK_PREFIX}${tab.command}`;
|
||||
private getKey(command: string): string {
|
||||
const start =
|
||||
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
||||
const end =
|
||||
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(':');
|
||||
}
|
||||
let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${
|
||||
tab.id
|
||||
} `;
|
||||
if (tab.task_before) {
|
||||
commandVariable += `task_before='${tab.task_before
|
||||
.replace(/'/g, "'\\''")
|
||||
.trim()}' `;
|
||||
if (index) {
|
||||
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
|
||||
} else {
|
||||
return command.substring(start, end);
|
||||
}
|
||||
if (tab.task_after) {
|
||||
commandVariable += `task_after='${tab.task_after
|
||||
.replace(/'/g, "'\\''")
|
||||
.trim()}' `;
|
||||
}
|
||||
|
||||
const crontab_job_string = `${commandVariable}${command}`;
|
||||
private make_command(tab: Crontab) {
|
||||
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
||||
return crontab_job_string;
|
||||
}
|
||||
|
||||
private async set_crontab(data?: { data: Crontab[]; total: number }) {
|
||||
const tabs = data ?? (await this.crontabs());
|
||||
private async set_crontab(needReloadSchedule: boolean = false) {
|
||||
const tabs = await this.crontabs();
|
||||
var crontab_string = '';
|
||||
tabs.data.forEach((tab) => {
|
||||
tabs.forEach((tab) => {
|
||||
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||
if (
|
||||
tab.isDisabled === 1 ||
|
||||
_schedule!.length !== 5 ||
|
||||
tab.extra_schedules?.length
|
||||
) {
|
||||
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
|
||||
crontab_string += '# ';
|
||||
crontab_string += tab.schedule;
|
||||
crontab_string += ' ';
|
||||
crontab_string += this.makeCommand(tab);
|
||||
crontab_string += this.make_command(tab);
|
||||
crontab_string += '\n';
|
||||
} else {
|
||||
crontab_string += tab.schedule;
|
||||
crontab_string += ' ';
|
||||
crontab_string += this.makeCommand(tab);
|
||||
crontab_string += this.make_command(tab);
|
||||
crontab_string += '\n';
|
||||
}
|
||||
});
|
||||
|
||||
await fs.writeFile(config.crontabFile, crontab_string);
|
||||
this.logger.silly(crontab_string);
|
||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||
|
||||
execSync(`crontab ${config.crontabFile}`);
|
||||
if (needReloadSchedule) {
|
||||
exec(`pm2 reload schedule`);
|
||||
}
|
||||
await CrontabModel.update({ saved: true }, { where: {} });
|
||||
}
|
||||
|
||||
@@ -624,19 +501,7 @@ export default class CronService {
|
||||
});
|
||||
}
|
||||
|
||||
public async autosave_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);
|
||||
public autosave_crontab() {
|
||||
return this.set_crontab();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
||||
import {
|
||||
initPosition,
|
||||
maxPosition,
|
||||
minPosition,
|
||||
stepPosition,
|
||||
} from '../data/env';
|
||||
import { FindOptions } from 'sequelize';
|
||||
|
||||
@Service()
|
||||
export default class CronViewService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public async create(payload: CrontabView): Promise<CrontabView> {
|
||||
let position = initPosition;
|
||||
const views = await this.list();
|
||||
if (views && views.length > 0 && views[views.length - 1].position) {
|
||||
position = views[views.length - 1].position as number;
|
||||
}
|
||||
position = position / 2;
|
||||
const tab = new CrontabView({ ...payload, position });
|
||||
const doc = await this.insert(tab);
|
||||
|
||||
await this.checkPosition(tab.position!);
|
||||
return doc;
|
||||
}
|
||||
|
||||
public async insert(payload: CrontabView): Promise<CrontabView> {
|
||||
return await CrontabViewModel.create(payload, { returning: true });
|
||||
}
|
||||
|
||||
public async update(payload: CrontabView): Promise<CrontabView> {
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new CrontabView({ ...doc, ...payload });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
public async updateDb(payload: CrontabView): Promise<CrontabView> {
|
||||
await CrontabViewModel.update(payload, { where: { id: payload.id } });
|
||||
return await this.getDb({ id: payload.id });
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
await CrontabViewModel.destroy({ where: { id: ids } });
|
||||
}
|
||||
|
||||
public async list(): Promise<CrontabView[]> {
|
||||
try {
|
||||
const result = await CrontabViewModel.findAll({
|
||||
where: {},
|
||||
order: [['position', 'DESC']],
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getDb(
|
||||
query: FindOptions<CrontabView>['where'],
|
||||
): Promise<CrontabView> {
|
||||
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
|
||||
if (!doc) {
|
||||
throw new Error(`CronView ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
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({
|
||||
id,
|
||||
fromIndex,
|
||||
toIndex,
|
||||
}: {
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
id: number;
|
||||
}): Promise<CrontabView> {
|
||||
let targetPosition: number;
|
||||
const isUpward = fromIndex > toIndex;
|
||||
const views = await this.list();
|
||||
if (toIndex === 0 || toIndex === views.length - 1) {
|
||||
targetPosition = isUpward
|
||||
? views[0].position! * 2
|
||||
: views[toIndex].position! / 2;
|
||||
} else {
|
||||
targetPosition = isUpward
|
||||
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
|
||||
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
|
||||
}
|
||||
const newDoc = await this.update({
|
||||
id,
|
||||
position: this.getPrecisionPosition(targetPosition),
|
||||
});
|
||||
|
||||
await this.checkPosition(targetPosition);
|
||||
return newDoc;
|
||||
}
|
||||
}
|
||||
+48
-193
@@ -8,20 +8,13 @@ import {
|
||||
DependenceTypes,
|
||||
unInstallDependenceCommandTypes,
|
||||
DependenceModel,
|
||||
GetDependenceCommandTypes,
|
||||
versionDependenceCommandTypes,
|
||||
} from '../data/dependence';
|
||||
import { spawn } from 'cross-spawn';
|
||||
import _ from 'lodash';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import { FindOptions, Op } from 'sequelize';
|
||||
import {
|
||||
fileExist,
|
||||
getPid,
|
||||
killTask,
|
||||
promiseExecSuccess,
|
||||
} from '../config/util';
|
||||
import { Op } from 'sequelize';
|
||||
import { concurrentRun } from '../config/util';
|
||||
import dayjs from 'dayjs';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
|
||||
@Service()
|
||||
export default class DependenceService {
|
||||
@@ -32,7 +25,7 @@ export default class DependenceService {
|
||||
|
||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||
const tabs = payloads.map((x) => {
|
||||
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
|
||||
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
|
||||
return tab;
|
||||
});
|
||||
const docs = await this.insert(tabs);
|
||||
@@ -53,7 +46,7 @@ export default class DependenceService {
|
||||
const tab = new Dependence({
|
||||
...doc,
|
||||
...other,
|
||||
status: DependenceStatus.queued,
|
||||
status: DependenceStatus.installing,
|
||||
});
|
||||
const newDoc = await this.updateDb(tab);
|
||||
this.installDependenceOneByOne([newDoc]);
|
||||
@@ -66,23 +59,12 @@ export default class DependenceService {
|
||||
}
|
||||
|
||||
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
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: [] },
|
||||
{ status: DependenceStatus.removing, log: [] },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
this.installDependenceOneByOne(docs, false, force);
|
||||
}
|
||||
return docs;
|
||||
}
|
||||
|
||||
@@ -91,23 +73,13 @@ export default class DependenceService {
|
||||
}
|
||||
|
||||
public async dependencies(
|
||||
{
|
||||
searchValue,
|
||||
type,
|
||||
status,
|
||||
}: { searchValue: string; type: string; status: string },
|
||||
sort: any = [],
|
||||
{ searchValue, type }: { searchValue: string; type: string },
|
||||
sort: any = { position: -1 },
|
||||
query: any = {},
|
||||
): Promise<Dependence[]> {
|
||||
let condition = {
|
||||
...query,
|
||||
type: DependenceTypes[type as any],
|
||||
};
|
||||
if (status) {
|
||||
condition.status = status.split(',').map(Number);
|
||||
}
|
||||
let condition = { ...query, type: DependenceTypes[type as any] };
|
||||
if (searchValue) {
|
||||
const encodeText = encodeURI(searchValue);
|
||||
const encodeText = encodeURIComponent(searchValue);
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchValue}%` },
|
||||
@@ -121,120 +93,78 @@ export default class DependenceService {
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await this.find(condition, sort);
|
||||
const result = await this.find(condition);
|
||||
return result as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public installDependenceOneByOne(
|
||||
private installDependenceOneByOne(
|
||||
docs: Dependence[],
|
||||
isInstall: boolean = true,
|
||||
force: boolean = false,
|
||||
) {
|
||||
docs.forEach((dep) => {
|
||||
this.installOrUninstallDependency(dep, isInstall, force);
|
||||
});
|
||||
concurrentRun(
|
||||
docs.map(
|
||||
(dep) => async () =>
|
||||
await this.installOrUninstallDependencies([dep], isInstall, force),
|
||||
),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.queued, log: [] },
|
||||
{ status: DependenceStatus.installing, log: [] },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
this.installDependenceOneByOne(docs, true, true);
|
||||
this.installDependenceOneByOne(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[]> {
|
||||
const docs = await DependenceModel.findAll({
|
||||
where: { ...query },
|
||||
order: [...sort, ['createdAt', 'DESC']],
|
||||
});
|
||||
private async find(query: any, sort?: any): Promise<Dependence[]> {
|
||||
const docs = await DependenceModel.findAll({ where: { ...query } });
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async getDb(
|
||||
query: FindOptions<Dependence>['where'],
|
||||
): Promise<Dependence> {
|
||||
public async getDb(query: any): Promise<Dependence> {
|
||||
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
||||
if (!doc) {
|
||||
throw new Error(`Dependency ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
return doc && (doc.get({ plain: true }) as Dependence);
|
||||
}
|
||||
|
||||
private async updateLog(ids: number[], log: string): Promise<void> {
|
||||
taskLimit.updateDepLog(async () => {
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
const doc = await DependenceModel.findOne({ where: { id: ids } });
|
||||
const newLog = doc?.log ? [...doc.log, log] : [log];
|
||||
await DependenceModel.update(
|
||||
{ log: newLog },
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public installOrUninstallDependency(
|
||||
dependency: Dependence,
|
||||
public installOrUninstallDependencies(
|
||||
dependencies: Dependence[],
|
||||
isInstall: boolean = true,
|
||||
force: boolean = false,
|
||||
) {
|
||||
return taskLimit.runDependeny(dependency, () => {
|
||||
return new Promise(async (resolve) => {
|
||||
if (taskLimit.firstDependencyId !== dependency.id) {
|
||||
return resolve(null);
|
||||
if (dependencies.length === 0) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
taskLimit.removeQueuedDependency(dependency);
|
||||
|
||||
const depIds = [dependency.id!];
|
||||
const status = isInstall
|
||||
? DependenceStatus.installing
|
||||
: DependenceStatus.removing;
|
||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||
|
||||
const socketMessageType = isInstall
|
||||
const socketMessageType = !force
|
||||
? 'installDependence'
|
||||
: 'uninstallDependence';
|
||||
let depName = dependency.name.trim();
|
||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
||||
const depRunCommand = (
|
||||
isInstall
|
||||
? InstallDependenceCommandTypes
|
||||
: unInstallDependenceCommandTypes
|
||||
)[dependency.type];
|
||||
)[dependencies[0].type as any];
|
||||
const actionText = isInstall ? '安装' : '删除';
|
||||
const depIds = dependencies.map((x) => x.id) as number[];
|
||||
const startTime = dayjs();
|
||||
|
||||
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
|
||||
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)}\n\n`;
|
||||
this.sockService.sendMessage({
|
||||
@@ -242,74 +172,9 @@ export default class DependenceService {
|
||||
message,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, message);
|
||||
await this.updateLog(depIds, message);
|
||||
|
||||
// 判断是否已经安装过依赖
|
||||
if (isInstall && !force) {
|
||||
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
|
||||
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
||||
let depVersion = '';
|
||||
if (depName.includes(depVersionStr)) {
|
||||
const symbolRegx = new RegExp(
|
||||
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
const dependenceProxyFileExist = await fileExist(
|
||||
config.dependenceProxyFile,
|
||||
);
|
||||
const proxyStr = dependenceProxyFileExist
|
||||
? `source ${config.dependenceProxyFile} &&`
|
||||
: '';
|
||||
const cp = spawn(
|
||||
`${proxyStr} ${depRunCommand} ${dependency.name.trim()}`,
|
||||
{
|
||||
shell: '/bin/bash',
|
||||
},
|
||||
);
|
||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||
|
||||
cp.stdout.on('data', async (data) => {
|
||||
this.sockService.sendMessage({
|
||||
@@ -317,7 +182,7 @@ export default class DependenceService {
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
await this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.stderr.on('data', async (data) => {
|
||||
@@ -326,7 +191,7 @@ export default class DependenceService {
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
await this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.on('error', async (err) => {
|
||||
@@ -335,10 +200,11 @@ export default class DependenceService {
|
||||
message: JSON.stringify(err),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, JSON.stringify(err));
|
||||
await this.updateLog(depIds, JSON.stringify(err));
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
cp.on('exit', async (code) => {
|
||||
cp.on('close', async (code) => {
|
||||
const endTime = dayjs();
|
||||
const isSucceed = code === 0;
|
||||
const resultText = isSucceed ? '成功' : '失败';
|
||||
@@ -351,7 +217,7 @@ export default class DependenceService {
|
||||
message,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, message);
|
||||
await this.updateLog(depIds, message);
|
||||
|
||||
let status = null;
|
||||
if (isSucceed) {
|
||||
@@ -363,17 +229,7 @@ export default class DependenceService {
|
||||
? 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 } },
|
||||
);
|
||||
}
|
||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||
|
||||
// 如果删除依赖成功或者强制删除
|
||||
if ((isSucceed || force) && !isInstall) {
|
||||
@@ -383,6 +239,5 @@ export default class DependenceService {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+39
-85
@@ -1,18 +1,11 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs/promises';
|
||||
import {
|
||||
Env,
|
||||
EnvModel,
|
||||
EnvStatus,
|
||||
initPosition,
|
||||
maxPosition,
|
||||
minPosition,
|
||||
stepPosition,
|
||||
} from '../data/env';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { FindOptions, Op } from 'sequelize';
|
||||
import * as fs from 'fs';
|
||||
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
|
||||
import _ from 'lodash';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
@Service()
|
||||
export default class EnvService {
|
||||
@@ -20,22 +13,17 @@ export default class EnvService {
|
||||
|
||||
public async create(payloads: Env[]): Promise<Env[]> {
|
||||
const envs = await this.envs();
|
||||
let position = initPosition;
|
||||
if (
|
||||
envs &&
|
||||
envs.length > 0 &&
|
||||
typeof envs[envs.length - 1].position === 'number'
|
||||
) {
|
||||
position = envs[envs.length - 1].position!;
|
||||
let position = initEnvPosition;
|
||||
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
|
||||
position = envs[envs.length - 1].position as number;
|
||||
}
|
||||
const tabs = payloads.map((x) => {
|
||||
position = position - stepPosition;
|
||||
position = position / 2;
|
||||
const tab = new Env({ ...x, position });
|
||||
return tab;
|
||||
});
|
||||
const docs = await this.insert(tabs);
|
||||
await this.set_envs();
|
||||
await this.checkPosition(tabs[tabs.length - 1].position!);
|
||||
return docs;
|
||||
}
|
||||
|
||||
@@ -49,9 +37,7 @@ export default class EnvService {
|
||||
}
|
||||
|
||||
public async update(payload: Env): Promise<Env> {
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new Env({ ...doc, ...payload });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
const newDoc = await this.updateDb(payload);
|
||||
await this.set_envs();
|
||||
return newDoc;
|
||||
}
|
||||
@@ -81,47 +67,28 @@ export default class EnvService {
|
||||
const envs = await this.envs();
|
||||
if (toIndex === 0 || toIndex === envs.length - 1) {
|
||||
targetPosition = isUpward
|
||||
? envs[0].position! + stepPosition
|
||||
: envs[toIndex].position! - stepPosition;
|
||||
? envs[0].position * 2
|
||||
: envs[toIndex].position / 2;
|
||||
} else {
|
||||
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({
|
||||
id,
|
||||
position: this.getPrecisionPosition(targetPosition),
|
||||
position: targetPosition,
|
||||
});
|
||||
|
||||
await this.checkPosition(targetPosition, envs[toIndex].position!);
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
private async checkPosition(position: number, edge: number = 0) {
|
||||
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||
if (
|
||||
precisionPosition < minPosition ||
|
||||
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[]> {
|
||||
public async envs(
|
||||
searchText: string = '',
|
||||
sort: any = { position: -1 },
|
||||
query: any = {},
|
||||
): Promise<Env[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const encodeText = encodeURI(searchText);
|
||||
const encodeText = encodeURIComponent(searchText);
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
@@ -145,10 +112,7 @@ export default class EnvService {
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await this.find(condition, [
|
||||
['position', 'DESC'],
|
||||
['createdAt', 'ASC'],
|
||||
]);
|
||||
const result = await this.find(condition, [['position', 'DESC']]);
|
||||
return result as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -163,12 +127,9 @@ export default class EnvService {
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
||||
public async getDb(query: any): Promise<Env> {
|
||||
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
||||
if (!doc) {
|
||||
throw new Error(`Env ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
return doc && (doc.get({ plain: true }) as Env);
|
||||
}
|
||||
|
||||
public async disabled(ids: string[]) {
|
||||
@@ -190,38 +151,31 @@ export default class EnvService {
|
||||
}
|
||||
|
||||
public async set_envs() {
|
||||
const envs = await this.envs('', {
|
||||
name: { [Op.not]: null },
|
||||
status: EnvStatus.normal,
|
||||
});
|
||||
const groups = groupBy(envs, 'name');
|
||||
const envs = await this.envs(
|
||||
'',
|
||||
{ position: -1 },
|
||||
{ name: { [Op.not]: null } },
|
||||
);
|
||||
const groups = _.groupBy(envs, 'name');
|
||||
let env_string = '';
|
||||
let js_env_string = '';
|
||||
let py_env_string = 'import os\n';
|
||||
for (const key in groups) {
|
||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||
const group = groups[key];
|
||||
|
||||
// 忽略不符合bash要求的环境变量名称
|
||||
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
|
||||
let value = group
|
||||
.map((x) => x.value)
|
||||
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
||||
let value = _(group)
|
||||
.filter((x) => x.status !== EnvStatus.disabled)
|
||||
.map('value')
|
||||
.join('&')
|
||||
.replace(/'/g, "'\\''")
|
||||
.replace(/(\\)[^\n]/g, '\\\\')
|
||||
.replace(/(\\$)/, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.trim();
|
||||
env_string += `export ${key}='${value}'\n`;
|
||||
const _env_value = `'${group
|
||||
.map((x) => x.value)
|
||||
.join('&')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "\\'")}'`;
|
||||
js_env_string += `process.env.${key}=${_env_value};\n`;
|
||||
py_env_string += `os.environ['${key}']=${_env_value}\n`;
|
||||
env_string += `export ${key}="${value}"\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
await fs.writeFile(config.envFile, env_string);
|
||||
await fs.writeFile(config.jsEnvFile, js_env_string);
|
||||
await fs.writeFile(config.pyEnvFile, py_env_string);
|
||||
fs.writeFileSync(config.envFile, env_string);
|
||||
}
|
||||
}
|
||||
|
||||
+66
-446
@@ -1,12 +1,11 @@
|
||||
import crypto from 'crypto';
|
||||
import got from 'got';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { parseBody, parseHeaders } from '../config/util';
|
||||
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 { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
|
||||
@Service()
|
||||
export default class NotificationService {
|
||||
@@ -18,30 +17,20 @@ export default class NotificationService {
|
||||
['goCqHttpBot', this.goCqHttpBot],
|
||||
['serverChan', this.serverChan],
|
||||
['pushDeer', this.pushDeer],
|
||||
['chat', this.chat],
|
||||
['bark', this.bark],
|
||||
['telegramBot', this.telegramBot],
|
||||
['dingtalkBot', this.dingtalkBot],
|
||||
['weWorkBot', this.weWorkBot],
|
||||
['weWorkApp', this.weWorkApp],
|
||||
['aibotk', this.aibotk],
|
||||
['iGot', this.iGot],
|
||||
['pushPlus', this.pushPlus],
|
||||
['wePlusBot', this.wePlusBot],
|
||||
['email', this.email],
|
||||
['pushMe', this.pushMe],
|
||||
['webhook', this.webhook],
|
||||
['lark', this.lark],
|
||||
['chronocat', this.chronocat],
|
||||
]);
|
||||
|
||||
private timeout = 10000;
|
||||
private title = '';
|
||||
private content = '';
|
||||
private params!: Omit<NotificationInfo, 'type'>;
|
||||
private gotOption = {
|
||||
timeout: 10000,
|
||||
retry: 1,
|
||||
};
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
@@ -58,7 +47,7 @@ export default class NotificationService {
|
||||
try {
|
||||
return await notificationModeAction?.call(this);
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -81,11 +70,11 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async gotify() {
|
||||
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
|
||||
try {
|
||||
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
||||
const res: any = await got
|
||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `title=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&message=${encodeURIComponent(
|
||||
@@ -96,34 +85,20 @@ export default class NotificationService {
|
||||
},
|
||||
})
|
||||
.json();
|
||||
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);
|
||||
}
|
||||
return typeof res.id === 'number';
|
||||
}
|
||||
|
||||
private async goCqHttpBot() {
|
||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: { message: `${this.title}\n${this.content}` },
|
||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||
})
|
||||
.json();
|
||||
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);
|
||||
}
|
||||
return res.retcode === 0;
|
||||
}
|
||||
|
||||
private async serverChan() {
|
||||
@@ -131,111 +106,53 @@ export default class NotificationService {
|
||||
const url = serverChanKey.startsWith('SCT')
|
||||
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
||||
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `title=${this.title}&desp=${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);
|
||||
}
|
||||
return res.errno === 0 || res.data.errno === 0;
|
||||
}
|
||||
|
||||
private async pushDeer() {
|
||||
const { pushDeerKey, pushDeerUrl } = this.params;
|
||||
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
||||
try {
|
||||
const { pushDeerKey } = this.params;
|
||||
const url = `https://api2.pushdeer.com/message/push`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
if (
|
||||
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() {
|
||||
const { chatUrl, chatToken } = this.params;
|
||||
const url = `${chatUrl}${chatToken}`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
if (res.success) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
return (
|
||||
res.content.result.length !== undefined && res.content.result.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
private async bark() {
|
||||
let {
|
||||
barkPush,
|
||||
barkIcon = '',
|
||||
barkSound = '',
|
||||
barkGroup = '',
|
||||
barkLevel = '',
|
||||
barkUrl = '',
|
||||
barkArchive = '',
|
||||
} = this.params;
|
||||
if (!barkPush.startsWith('http')) {
|
||||
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
||||
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
||||
barkPush = `https://api.day.app/${barkPush}`;
|
||||
}
|
||||
const url = `${barkPush}`;
|
||||
const body = {
|
||||
title: this.title,
|
||||
body: this.content,
|
||||
icon: barkIcon,
|
||||
sound: barkSound,
|
||||
group: barkGroup,
|
||||
isArchive: barkArchive,
|
||||
level: barkLevel,
|
||||
url: barkUrl,
|
||||
};
|
||||
try {
|
||||
const url = `${barkPush}/${encodeURIComponent(
|
||||
this.title,
|
||||
)}/${encodeURIComponent(
|
||||
this.content,
|
||||
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
json: body,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
.get(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.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);
|
||||
}
|
||||
return res.code === 200;
|
||||
}
|
||||
|
||||
private async telegramBot() {
|
||||
@@ -248,8 +165,8 @@ export default class NotificationService {
|
||||
telegramBotUserId,
|
||||
} = this.params;
|
||||
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
|
||||
const url = `${
|
||||
telegramBotApiHost ? telegramBotApiHost : 'https://api.telegram.org'
|
||||
const url = `https://${
|
||||
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
|
||||
}/bot${telegramBotToken}/sendMessage`;
|
||||
let agent;
|
||||
if (telegramBotProxyHost && telegramBotProxyPort) {
|
||||
@@ -267,23 +184,16 @@ export default class NotificationService {
|
||||
https: httpsAgent,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
agent,
|
||||
})
|
||||
.json();
|
||||
if (res.ok) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
return !!res.ok;
|
||||
}
|
||||
|
||||
private async dingtalkBot() {
|
||||
@@ -297,10 +207,10 @@ export default class NotificationService {
|
||||
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
||||
}
|
||||
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
@@ -309,24 +219,16 @@ export default class NotificationService {
|
||||
},
|
||||
})
|
||||
.json();
|
||||
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);
|
||||
}
|
||||
return res.errcode === 0;
|
||||
}
|
||||
|
||||
private async weWorkBot() {
|
||||
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
||||
this.params;
|
||||
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||
try {
|
||||
const { weWorkBotKey } = this.params;
|
||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
@@ -335,25 +237,18 @@ export default class NotificationService {
|
||||
},
|
||||
})
|
||||
.json();
|
||||
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);
|
||||
}
|
||||
return res.errcode === 0;
|
||||
}
|
||||
|
||||
private async weWorkApp() {
|
||||
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
||||
this.params;
|
||||
const { weWorkAppKey } = this.params;
|
||||
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||
weWorkAppKey.split(',');
|
||||
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
|
||||
const tokenRes: any = await got
|
||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
||||
const { access_token } = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
corpid,
|
||||
corpsecret,
|
||||
@@ -399,12 +294,12 @@ export default class NotificationService {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(
|
||||
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
||||
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
|
||||
{
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
touser,
|
||||
agentid,
|
||||
@@ -415,93 +310,31 @@ export default class NotificationService {
|
||||
)
|
||||
.json();
|
||||
|
||||
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);
|
||||
}
|
||||
return res.errcode === 0;
|
||||
}
|
||||
|
||||
private async iGot() {
|
||||
const { iGotPushKey } = this.params;
|
||||
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `title=${this.title}&content=${this.content}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
|
||||
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);
|
||||
}
|
||||
return res.ret === 0;
|
||||
}
|
||||
|
||||
private async pushPlus() {
|
||||
const { pushPlusToken, pushPlusUser } = this.params;
|
||||
const url = `https://www.pushplus.plus/send`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
token: `${pushPlusToken}`,
|
||||
title: `${this.title}`,
|
||||
@@ -511,84 +344,11 @@ export default class NotificationService {
|
||||
})
|
||||
.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);
|
||||
}
|
||||
return res.code === 200;
|
||||
}
|
||||
|
||||
private async email() {
|
||||
const { emailPass, emailService, emailUser } = this.params;
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: emailService,
|
||||
auth: {
|
||||
@@ -606,146 +366,6 @@ export default class NotificationService {
|
||||
|
||||
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 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() {
|
||||
const {
|
||||
webhookUrl,
|
||||
webhookBody,
|
||||
webhookHeaders,
|
||||
webhookMethod,
|
||||
webhookContentType,
|
||||
} = this.params;
|
||||
|
||||
if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) {
|
||||
throw new Error('Url 或者 Body 中必须包含 $title');
|
||||
}
|
||||
|
||||
const headers = parseHeaders(webhookHeaders);
|
||||
const body = parseBody(webhookBody, webhookContentType, (v) =>
|
||||
v?.replaceAll('$title', this.title)?.replaceAll('$content', this.content),
|
||||
);
|
||||
const bodyParam = this.formatBody(webhookContentType, body);
|
||||
const options = {
|
||||
method: webhookMethod,
|
||||
headers,
|
||||
...this.gotOption,
|
||||
allowGetBody: true,
|
||||
...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);
|
||||
}
|
||||
}
|
||||
|
||||
private formatBody(contentType: string, body: any): object {
|
||||
if (!body) return {};
|
||||
switch (contentType) {
|
||||
case 'application/json':
|
||||
return { json: body };
|
||||
case 'multipart/form-data':
|
||||
return { form: body };
|
||||
case 'application/x-www-form-urlencoded':
|
||||
case 'text/plain':
|
||||
return { body };
|
||||
}
|
||||
return {};
|
||||
return !!info.messageId;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-13
@@ -26,7 +26,7 @@ export default class OpenService {
|
||||
|
||||
public async insert(payload: App): Promise<App> {
|
||||
const doc = await AppModel.create(payload, { returning: true });
|
||||
return doc.get({ plain: true });
|
||||
return doc.get({ plain: true }) as App;
|
||||
}
|
||||
|
||||
public async update(payload: App): Promise<App> {
|
||||
@@ -45,10 +45,7 @@ export default class OpenService {
|
||||
|
||||
public async getDb(query: any): Promise<App> {
|
||||
const doc: any = await AppModel.findOne({ where: query });
|
||||
if (!doc) {
|
||||
throw new Error(`App ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
return doc && (doc.get({ plain: true }) as App);
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
@@ -78,7 +75,7 @@ export default class OpenService {
|
||||
): Promise<App[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const encodeText = encodeURI(searchText);
|
||||
const encodeText = encodeURIComponent(searchText);
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
@@ -122,7 +119,7 @@ export default class OpenService {
|
||||
(x) => x.expiration >= timestamp,
|
||||
);
|
||||
let tokens = invalidTokens;
|
||||
if (invalidTokens.length >= 5) {
|
||||
if (invalidTokens.length > 5) {
|
||||
tokens = [
|
||||
...invalidTokens.slice(0, 4),
|
||||
{ ...invalidTokens[4], expiration },
|
||||
@@ -148,7 +145,7 @@ export default class OpenService {
|
||||
}
|
||||
}
|
||||
|
||||
public async generateSystemToken(): Promise<{
|
||||
public async findSystemToken(): Promise<{
|
||||
value: string;
|
||||
expiration: number;
|
||||
}> {
|
||||
@@ -161,13 +158,21 @@ export default class OpenService {
|
||||
scopes: ['crons', 'system'],
|
||||
} as App);
|
||||
}
|
||||
const { data } = await this.authToken({
|
||||
const nowTime = Math.round(Date.now() / 1000);
|
||||
let token;
|
||||
if (
|
||||
!systemApp.tokens ||
|
||||
!systemApp.tokens.length ||
|
||||
nowTime > [...systemApp.tokens].pop()!.expiration
|
||||
) {
|
||||
const authToken = await this.authToken({
|
||||
client_id: systemApp.client_id,
|
||||
client_secret: systemApp.client_secret,
|
||||
});
|
||||
return {
|
||||
...data,
|
||||
value: data.token,
|
||||
};
|
||||
token = authToken.data;
|
||||
} else {
|
||||
token = [...systemApp.tokens].pop();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-58
@@ -1,19 +1,17 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
|
||||
import {
|
||||
ToadScheduler,
|
||||
LongIntervalJob,
|
||||
AsyncTask,
|
||||
SimpleIntervalSchedule,
|
||||
Task,
|
||||
} from 'toad-scheduler';
|
||||
import dayjs from 'dayjs';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import { spawn } from 'cross-spawn';
|
||||
|
||||
export interface ScheduleTaskType {
|
||||
id?: number;
|
||||
interface ScheduleTaskType {
|
||||
id: number;
|
||||
command: string;
|
||||
name?: string;
|
||||
schedule?: string;
|
||||
@@ -42,39 +40,28 @@ export default class ScheduleService {
|
||||
|
||||
private maxBuffer = 200 * 1024 * 1024;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
async runTask(
|
||||
command: string,
|
||||
callbacks: TaskCallbacks = {},
|
||||
params: {
|
||||
schedule?: string;
|
||||
name?: string;
|
||||
command?: string;
|
||||
},
|
||||
completionTime: 'start' | 'end' = 'end',
|
||||
) {
|
||||
return taskLimit.runWithCronLimit(() => {
|
||||
async runTask(command: string, callbacks: TaskCallbacks = {}) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
|
||||
|
||||
try {
|
||||
const startTime = dayjs();
|
||||
await callbacks.onBefore?.(startTime);
|
||||
|
||||
const cp = spawn(command, { shell: '/bin/bash' });
|
||||
|
||||
// TODO:
|
||||
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',
|
||||
this.logger.error(
|
||||
'执行任务 %s 失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
data.toString(),
|
||||
);
|
||||
await callbacks.onError?.(data.toString());
|
||||
@@ -82,32 +69,41 @@ export default class ScheduleService {
|
||||
|
||||
cp.on('error', async (err) => {
|
||||
this.logger.error(
|
||||
'[panel][创建任务失败] 命令: %s, 错误信息: %j',
|
||||
'创建任务 %s 失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
err,
|
||||
);
|
||||
await callbacks.onError?.(JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('exit', async (code) => {
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||
);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
const endTime = dayjs();
|
||||
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||
await callbacks.onEnd?.(
|
||||
cp,
|
||||
endTime,
|
||||
endTime.diff(startTime, 'seconds'),
|
||||
);
|
||||
resolve({ ...params, pid: cp.pid, code });
|
||||
resolve(null);
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||
await this.logger.error(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
error,
|
||||
);
|
||||
await callbacks.onError?.(JSON.stringify(error));
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async createCronTask(
|
||||
@@ -117,7 +113,7 @@ export default class ScheduleService {
|
||||
) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info(
|
||||
'[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
|
||||
'[创建cron任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
||||
_id,
|
||||
schedule,
|
||||
name,
|
||||
@@ -127,30 +123,19 @@ export default class ScheduleService {
|
||||
this.scheduleStacks.set(
|
||||
_id,
|
||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
schedule,
|
||||
command,
|
||||
});
|
||||
await this.runTask(command, callbacks);
|
||||
}),
|
||||
);
|
||||
|
||||
if (runImmediately) {
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
schedule,
|
||||
command,
|
||||
});
|
||||
await this.runTask(command, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info('[panel][取消定时任务], 任务名: %s', name);
|
||||
if (this.scheduleStacks.has(_id)) {
|
||||
this.scheduleStacks.get(_id)?.cancel();
|
||||
this.scheduleStacks.delete(_id);
|
||||
}
|
||||
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
||||
}
|
||||
|
||||
async createIntervalTask(
|
||||
@@ -161,30 +146,30 @@ export default class ScheduleService {
|
||||
) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info(
|
||||
'[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s',
|
||||
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
|
||||
_id,
|
||||
name,
|
||||
command,
|
||||
);
|
||||
const task = new Task(
|
||||
const task = new AsyncTask(
|
||||
name,
|
||||
() => {
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
command,
|
||||
async () => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
await this.runTask(command, callbacks);
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
this.logger.error(
|
||||
'[执行任务失败] 命令: %s, 错误信息: %j',
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
err,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const job = new LongIntervalJob(
|
||||
{ runImmediately: false, ...schedule },
|
||||
{ ...schedule, runImmediately: false },
|
||||
task,
|
||||
_id,
|
||||
);
|
||||
@@ -192,16 +177,13 @@ export default class ScheduleService {
|
||||
this.intervalSchedule.addIntervalJob(job);
|
||||
|
||||
if (runImmediately) {
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
command,
|
||||
});
|
||||
await this.runTask(command, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name);
|
||||
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
|
||||
this.intervalSchedule.removeById(_id);
|
||||
}
|
||||
|
||||
|
||||
+20
-30
@@ -1,12 +1,11 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import path, { join } from 'path';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import SockService from './sock';
|
||||
import CronService from './cron';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import config from '../config';
|
||||
import { TASK_COMMAND } from '../config/const';
|
||||
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
|
||||
|
||||
@Service()
|
||||
export default class ScriptService {
|
||||
@@ -20,7 +19,9 @@ export default class ScriptService {
|
||||
private taskCallbacks(filePath: string): TaskCallbacks {
|
||||
return {
|
||||
onEnd: async (cp, endTime, diff) => {
|
||||
await rmPath(filePath);
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch (error) {}
|
||||
},
|
||||
onError: async (message: string) => {
|
||||
this.sockService.sendMessage({
|
||||
@@ -39,35 +40,24 @@ export default class ScriptService {
|
||||
|
||||
public async runScript(filePath: string) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
const command = `${TASK_COMMAND} ${relativePath} now`;
|
||||
const pid = await this.scheduleService.runTask(
|
||||
`real_time=true ${command}`,
|
||||
this.taskCallbacks(filePath),
|
||||
{ command },
|
||||
'start',
|
||||
);
|
||||
|
||||
return { code: 200, data: pid };
|
||||
}
|
||||
|
||||
public async stopScript(filePath: string, pid: number) {
|
||||
if (!pid) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
|
||||
}
|
||||
try {
|
||||
await killTask(pid);
|
||||
} catch (error) {}
|
||||
const command = `task -l ${relativePath} now`;
|
||||
this.scheduleService.runTask(command, this.taskCallbacks(filePath));
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async getFile(filePath: string, fileName: string) {
|
||||
let _filePath = join(config.scriptPath, filePath, fileName);
|
||||
if (filePath.startsWith(config.dataPath)) {
|
||||
_filePath = join(filePath, fileName);
|
||||
}
|
||||
const content = await getFileContentByName(_filePath);
|
||||
return content;
|
||||
public async stopScript(filePath: string) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
|
||||
|
||||
const str = err ? `\n${err}` : '';
|
||||
this.sockService.sendMessage({
|
||||
type: 'manuallyRunScript',
|
||||
message: `${str}\n## 执行结束... ${new Date()
|
||||
.toLocaleString('zh', { hour12: false })
|
||||
.replace(' 24:', ' 00:')} `,
|
||||
});
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SockMessage } from '../data/sock';
|
||||
export default class SockService {
|
||||
private clients: Connection[] = [];
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public getClients() {
|
||||
return this.clients;
|
||||
|
||||
+47
-88
@@ -1,47 +1,20 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { Subscription } from '../data/subscription';
|
||||
import { formatUrl } from '../config/subscription';
|
||||
import config from '../config';
|
||||
import { fileExist, rmPath } from '../config/util';
|
||||
|
||||
@Service()
|
||||
export default class SshKeyService {
|
||||
private homedir = os.homedir();
|
||||
private sshPath = config.sshdPath;
|
||||
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');
|
||||
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
|
||||
private sshPath = path.resolve(this.homedir, '.ssh');
|
||||
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
||||
this.initSshConfigFile();
|
||||
}
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
private async initSshConfigFile() {
|
||||
let config = '';
|
||||
const _exist = await fileExist(this.sshConfigFilePath);
|
||||
if (_exist) {
|
||||
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
|
||||
} else {
|
||||
await fs.writeFile(this.sshConfigFilePath, '');
|
||||
}
|
||||
if (!config.includes(this.sshConfigHeader)) {
|
||||
await fs.writeFile(
|
||||
this.sshConfigFilePath,
|
||||
`${this.sshConfigHeader}\n\n${config}`,
|
||||
{ encoding: 'utf-8' },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async generatePrivateKeyFile(
|
||||
alias: string,
|
||||
key: string,
|
||||
): Promise<void> {
|
||||
private generatePrivateKeyFile(alias: string, key: string): void {
|
||||
try {
|
||||
await fs.writeFile(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
|
||||
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
|
||||
encoding: 'utf8',
|
||||
mode: '400',
|
||||
});
|
||||
@@ -50,76 +23,62 @@ export default class SshKeyService {
|
||||
}
|
||||
}
|
||||
|
||||
private async removePrivateKeyFile(alias: string): Promise<void> {
|
||||
private getConfigRegx(alias: string) {
|
||||
return new RegExp(
|
||||
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
|
||||
'g',
|
||||
);
|
||||
}
|
||||
|
||||
private removePrivateKeyFile(alias: string): void {
|
||||
try {
|
||||
const filePath = path.join(this.sshPath, alias);
|
||||
await rmPath(filePath);
|
||||
fs.unlinkSync(`${this.sshPath}/${alias}`);
|
||||
} catch (error) {
|
||||
this.logger.error('删除私钥文件失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async generateSingleSshConfig(
|
||||
alias: string,
|
||||
host: string,
|
||||
proxy?: string,
|
||||
) {
|
||||
if (host === 'github.com') {
|
||||
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa`;
|
||||
}
|
||||
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 fs.writeFile(
|
||||
`${path.join(this.sshPath, `${alias}.config`)}`,
|
||||
config,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
private generateSingleSshConfig(alias: string, host: string): string {
|
||||
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
|
||||
}
|
||||
|
||||
private async removeSshConfig(alias: string) {
|
||||
private generateSshConfig(configs: string[]) {
|
||||
try {
|
||||
const filePath = path.join(this.sshPath, `${alias}.config`);
|
||||
await rmPath(filePath);
|
||||
for (const config of configs) {
|
||||
fs.appendFileSync(this.sshConfigFilePath, config, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('写入ssh配置文件失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
private removeSshConfig(alias: string) {
|
||||
try {
|
||||
const configRegx = this.getConfigRegx(alias);
|
||||
const data = fs
|
||||
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
|
||||
.replace(configRegx, '')
|
||||
.replace(/\n[\n]+/g, '\n');
|
||||
fs.writeFileSync(this.sshConfigFilePath, data, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`删除ssh配置文件${alias}失败`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public async addSSHKey(
|
||||
key: string,
|
||||
alias: string,
|
||||
host: string,
|
||||
proxy?: string,
|
||||
): Promise<void> {
|
||||
await this.generatePrivateKeyFile(alias, key);
|
||||
await this.generateSingleSshConfig(alias, host, proxy);
|
||||
public addSSHKey(key: string, alias: string, host: string): void {
|
||||
this.generatePrivateKeyFile(alias, key);
|
||||
const config = this.generateSingleSshConfig(alias, host);
|
||||
this.removeSshConfig(alias);
|
||||
this.generateSshConfig([config]);
|
||||
}
|
||||
|
||||
public async removeSSHKey(alias: string, host: string, 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);
|
||||
}
|
||||
}
|
||||
public removeSSHKey(alias: string, host: string): void {
|
||||
this.removePrivateKeyFile(alias);
|
||||
const config = this.generateSingleSshConfig(alias, host);
|
||||
this.removeSshConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
+194
-94
@@ -6,29 +6,29 @@ import {
|
||||
SubscriptionModel,
|
||||
SubscriptionStatus,
|
||||
} from '../data/subscription';
|
||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||
import {
|
||||
ChildProcessWithoutNullStreams,
|
||||
exec,
|
||||
execSync,
|
||||
spawn,
|
||||
} from 'child_process';
|
||||
import fs from 'fs';
|
||||
import cron_parser from 'cron-parser';
|
||||
import {
|
||||
getFileContentByName,
|
||||
concurrentRun,
|
||||
fileExist,
|
||||
createFile,
|
||||
killTask,
|
||||
handleLogPath,
|
||||
promiseExec,
|
||||
rmPath,
|
||||
} from '../config/util';
|
||||
import fs from 'fs/promises';
|
||||
import { FindOptions, Op } from 'sequelize';
|
||||
import path, { join } from 'path';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { Op } from 'sequelize';
|
||||
import path from 'path';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
||||
import SockService from './sock';
|
||||
import SshKeyService from './sshKey';
|
||||
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';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionService {
|
||||
@@ -37,16 +37,32 @@ export default class SubscriptionService {
|
||||
private scheduleService: ScheduleService,
|
||||
private sockService: SockService,
|
||||
private sshKeyService: SshKeyService,
|
||||
private crontabService: CrontabService,
|
||||
) {}
|
||||
|
||||
public async list(searchText?: string): Promise<Subscription[]> {
|
||||
let query = {};
|
||||
if (searchText) {
|
||||
const textArray = searchText.split(':');
|
||||
switch (textArray[0]) {
|
||||
case 'name':
|
||||
case 'command':
|
||||
case 'schedule':
|
||||
case 'label':
|
||||
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
|
||||
query = {
|
||||
[column]: {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${textArray[1]}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
||||
],
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
{ [Op.like]: `%${encodeURI(searchText)}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||
],
|
||||
};
|
||||
query = {
|
||||
@@ -55,33 +71,80 @@ export default class SubscriptionService {
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
url: reg,
|
||||
command: reg,
|
||||
},
|
||||
{
|
||||
schedule: reg,
|
||||
},
|
||||
{
|
||||
labels: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await SubscriptionModel.findAll({
|
||||
where: query,
|
||||
order: [
|
||||
['is_disabled', 'ASC'],
|
||||
['createdAt', 'DESC'],
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
return result;
|
||||
return result as any;
|
||||
} catch (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(
|
||||
doc: Subscription,
|
||||
needCreate = true,
|
||||
needAddKey = true,
|
||||
runImmediately = false,
|
||||
) {
|
||||
const { url } = formatUrl(doc);
|
||||
const { url, host } = this.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 = formatCommand(doc, url as string);
|
||||
doc.command = this.formatCommand(doc, url as string);
|
||||
|
||||
if (doc.schedule_type === 'crontab') {
|
||||
this.scheduleService.cancelCronTask(doc as any);
|
||||
@@ -91,9 +154,9 @@ export default class SubscriptionService {
|
||||
this.taskCallbacks(doc),
|
||||
runImmediately,
|
||||
));
|
||||
} else if (doc.interval_schedule) {
|
||||
} else {
|
||||
this.scheduleService.cancelIntervalTask(doc as any);
|
||||
const { type, value } = doc.interval_schedule;
|
||||
const { type, value } = doc.interval_schedule as any;
|
||||
needCreate &&
|
||||
(await this.scheduleService.createIntervalTask(
|
||||
doc as any,
|
||||
@@ -104,9 +167,28 @@ export default class SubscriptionService {
|
||||
}
|
||||
}
|
||||
|
||||
public async setSshConfig() {
|
||||
const docs = await SubscriptionModel.findAll();
|
||||
await this.sshKeyService.setSshConfig(docs);
|
||||
private async promiseExec(command: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
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 {
|
||||
@@ -121,7 +203,7 @@ export default class SubscriptionService {
|
||||
},
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
const absolutePath = await handleLogPath(
|
||||
const absolutePath = await this.handleLogPath(
|
||||
logPath as string,
|
||||
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
|
||||
);
|
||||
@@ -130,15 +212,15 @@ export default class SubscriptionService {
|
||||
let beforeStr = '';
|
||||
try {
|
||||
if (doc.sub_before) {
|
||||
await fs.appendFile(absolutePath, `\n## 执行before命令...\n\n`);
|
||||
beforeStr = await promiseExec(doc.sub_before);
|
||||
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
|
||||
beforeStr = await this.promiseExec(doc.sub_before);
|
||||
}
|
||||
} catch (error: any) {
|
||||
beforeStr =
|
||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
||||
}
|
||||
if (beforeStr) {
|
||||
await fs.appendFile(absolutePath, `${beforeStr}\n`);
|
||||
fs.appendFileSync(absolutePath, `${beforeStr}\n`);
|
||||
}
|
||||
},
|
||||
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
|
||||
@@ -151,28 +233,28 @@ export default class SubscriptionService {
|
||||
},
|
||||
onEnd: async (cp, endTime, diff) => {
|
||||
const sub = await this.getDb({ id: doc.id });
|
||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
||||
|
||||
// 执行 sub_after
|
||||
let afterStr = '';
|
||||
try {
|
||||
if (sub.sub_after) {
|
||||
await fs.appendFile(absolutePath, `\n\n## 执行after命令...\n\n`);
|
||||
afterStr = await promiseExec(sub.sub_after);
|
||||
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
|
||||
afterStr = await this.promiseExec(sub.sub_after);
|
||||
}
|
||||
} catch (error: any) {
|
||||
afterStr =
|
||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
||||
}
|
||||
if (afterStr) {
|
||||
await fs.appendFile(absolutePath, `${afterStr}\n`);
|
||||
fs.appendFileSync(absolutePath, `${afterStr}\n`);
|
||||
}
|
||||
|
||||
await fs.appendFile(
|
||||
fs.appendFileSync(
|
||||
absolutePath,
|
||||
`\n## 执行结束... ${endTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)} 耗时 ${diff} 秒${LOG_END_SYMBOL}`,
|
||||
)} 耗时 ${diff} 秒`,
|
||||
);
|
||||
|
||||
await SubscriptionModel.update(
|
||||
@@ -188,13 +270,13 @@ export default class SubscriptionService {
|
||||
},
|
||||
onError: async (message: string) => {
|
||||
const sub = await this.getDb({ id: doc.id });
|
||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||
await fs.appendFile(absolutePath, `\n${message}`);
|
||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
},
|
||||
onLog: async (message: string) => {
|
||||
const sub = await this.getDb({ id: doc.id });
|
||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||
await fs.appendFile(absolutePath, `\n${message}`);
|
||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -203,7 +285,6 @@ export default class SubscriptionService {
|
||||
const tab = new Subscription(payload);
|
||||
const doc = await this.insert(tab);
|
||||
await this.handleTask(doc);
|
||||
await this.setSshConfig();
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -212,11 +293,8 @@ export default class SubscriptionService {
|
||||
}
|
||||
|
||||
public async update(payload: Subscription): Promise<Subscription> {
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new Subscription({ ...doc, ...payload });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
const newDoc = await this.updateDb(payload);
|
||||
await this.handleTask(newDoc, !newDoc.is_disabled);
|
||||
await this.setSshConfig();
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
@@ -256,34 +334,17 @@ export default class SubscriptionService {
|
||||
);
|
||||
}
|
||||
|
||||
public async remove(ids: number[], query: { force?: boolean }) {
|
||||
public async remove(ids: number[]) {
|
||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
await this.handleTask(doc, false);
|
||||
await this.handleTask(doc, false, false);
|
||||
}
|
||||
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);
|
||||
await rmPath(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getDb(
|
||||
query: FindOptions<Subscription>['where'],
|
||||
): 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 getDb(query: any): Promise<Subscription> {
|
||||
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Subscription);
|
||||
}
|
||||
|
||||
public async run(ids: number[]) {
|
||||
@@ -291,9 +352,10 @@ export default class SubscriptionService {
|
||||
{ status: SubscriptionStatus.queued },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
ids.forEach((id) => {
|
||||
this.runSingle(id);
|
||||
});
|
||||
concurrentRun(
|
||||
ids.map((id) => async () => await this.runSingle(id)),
|
||||
10,
|
||||
);
|
||||
}
|
||||
|
||||
public async stop(ids: number[]) {
|
||||
@@ -301,11 +363,20 @@ export default class SubscriptionService {
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
try {
|
||||
await killTask(doc.pid);
|
||||
process.kill(-doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
this.logger.silly(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(
|
||||
@@ -314,37 +385,69 @@ 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) {
|
||||
const subscription = await this.getDb({ id: subscriptionId });
|
||||
if (subscription.status !== SubscriptionStatus.queued) {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = formatCommand(subscription);
|
||||
const command = this.formatCommand(subscription);
|
||||
|
||||
this.scheduleService.runTask(command, this.taskCallbacks(subscription), {
|
||||
name: subscription.name,
|
||||
schedule: subscription.schedule,
|
||||
await this.scheduleService.runTask(
|
||||
command,
|
||||
});
|
||||
this.taskCallbacks(subscription),
|
||||
);
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
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, false);
|
||||
}
|
||||
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
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);
|
||||
}
|
||||
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async log(id: number) {
|
||||
@@ -353,8 +456,8 @@ export default class SubscriptionService {
|
||||
return '';
|
||||
}
|
||||
|
||||
const absolutePath = await handleLogPath(doc.log_path as string);
|
||||
return await getFileContentByName(absolutePath);
|
||||
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
||||
return getFileContentByName(absolutePath);
|
||||
}
|
||||
|
||||
public async logs(id: number) {
|
||||
@@ -366,18 +469,15 @@ export default class SubscriptionService {
|
||||
if (doc.log_path) {
|
||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||
const dir = path.resolve(config.logPath, relativeDir);
|
||||
const _exist = await fileExist(dir);
|
||||
if (_exist) {
|
||||
let files = await fs.readdir(dir);
|
||||
return (
|
||||
await Promise.all(
|
||||
files.map(async (x) => ({
|
||||
if (existsSync(dir)) {
|
||||
let files = await promises.readdir(dir);
|
||||
return files
|
||||
.map((x) => ({
|
||||
filename: x,
|
||||
directory: relativeDir.replace(config.logPath, ''),
|
||||
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(),
|
||||
})),
|
||||
)
|
||||
).sort((a, b) => b.time - a.time);
|
||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
||||
}))
|
||||
.sort((a, b) => b.time - a.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
-305
@@ -1,38 +1,15 @@
|
||||
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 { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import { TASK_COMMAND } from '../config/const';
|
||||
import {
|
||||
getPid,
|
||||
killTask,
|
||||
parseContentVersion,
|
||||
parseVersion,
|
||||
promiseExec,
|
||||
readDirs,
|
||||
} from '../config/util';
|
||||
import {
|
||||
DependenceModel,
|
||||
DependenceStatus,
|
||||
DependenceTypes,
|
||||
} from '../data/dependence';
|
||||
import * as fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import {
|
||||
AuthDataType,
|
||||
SystemInfo,
|
||||
SystemInstance,
|
||||
SystemModel,
|
||||
SystemModelInfo,
|
||||
} from '../data/system';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import NotificationService from './notify';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import ScheduleService from './schedule';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import got from 'got';
|
||||
|
||||
@Service()
|
||||
export default class SystemService {
|
||||
@@ -45,23 +22,20 @@ export default class SystemService {
|
||||
private sockService: SockService,
|
||||
) {}
|
||||
|
||||
public async getSystemConfig() {
|
||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||
return doc;
|
||||
public async getLogRemoveFrequency() {
|
||||
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
|
||||
return doc || {};
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
|
||||
await SystemModel.upsert({ ...payload });
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
await AuthModel.upsert({ ...payload });
|
||||
const doc = await this.getDb({ type: payload.type });
|
||||
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 getDb(query: any): Promise<any> {
|
||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as any);
|
||||
}
|
||||
|
||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||
@@ -82,203 +56,56 @@ export default class SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateLogRemoveFrequency(info: SystemModelInfo) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
public async updateLogRemoveFrequency(frequency: number) {
|
||||
const oDoc = await this.getLogRemoveFrequency();
|
||||
const result = await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
type: AuthDataType.removeLogFrequency,
|
||||
info: { frequency },
|
||||
});
|
||||
const cron = {
|
||||
id: result.id || NaN,
|
||||
id: result.id,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${info.logRemoveFrequency}`,
|
||||
command: `ql rmlog ${frequency}`,
|
||||
};
|
||||
if (oDoc.info?.logRemoveFrequency) {
|
||||
await this.scheduleService.cancelIntervalTask(cron);
|
||||
}
|
||||
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {
|
||||
this.scheduleService.createIntervalTask(cron, {
|
||||
days: info.logRemoveFrequency,
|
||||
if (frequency > 0) {
|
||||
await this.scheduleService.createIntervalTask(cron, {
|
||||
days: frequency,
|
||||
runImmediately: 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
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,
|
||||
},
|
||||
);
|
||||
return { code: 200, data: { ...cron } };
|
||||
}
|
||||
|
||||
public async checkUpdate() {
|
||||
try {
|
||||
const currentVersionContent = await parseVersion(config.versionFile);
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
||||
|
||||
let lastVersionContent;
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
let lastVersion = '';
|
||||
let lastLog = '';
|
||||
try {
|
||||
const result = await got.get(
|
||||
`${config.lastVersionFile}?t=${Date.now()}`,
|
||||
{
|
||||
timeout: 30000,
|
||||
},
|
||||
);
|
||||
lastVersionContent = await parseContentVersion(result.body);
|
||||
const result = await got.get(config.lastVersionFile, {
|
||||
timeout: 6000,
|
||||
retry: 0,
|
||||
});
|
||||
const lastVersionFileContent = result.body;
|
||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||
lastLog = lastVersionFileContent.match(logRegx)
|
||||
? lastVersionFileContent.match(logRegx)![1]
|
||||
: '';
|
||||
} catch (error) {}
|
||||
|
||||
if (!lastVersionContent) {
|
||||
lastVersionContent = currentVersionContent;
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
hasNewVersion: this.checkHasNewVersion(
|
||||
currentVersionContent.version,
|
||||
lastVersionContent.version,
|
||||
),
|
||||
lastVersion: lastVersionContent.version,
|
||||
lastLog: lastVersionContent.changeLog,
|
||||
lastLogLink: lastVersionContent.changeLogLink,
|
||||
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
||||
lastVersion,
|
||||
lastLog,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
@@ -309,8 +136,12 @@ export default class SystemService {
|
||||
}
|
||||
|
||||
public async updateSystem() {
|
||||
const cp = spawn('real_time=true ql update false', { shell: '/bin/bash' });
|
||||
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: `开始更新系统`,
|
||||
});
|
||||
cp.stdout.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
@@ -335,13 +166,6 @@ export default class SystemService {
|
||||
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 }) {
|
||||
const isSuccess = await this.notificationService.notify(title, content);
|
||||
if (isSuccess) {
|
||||
@@ -350,83 +174,4 @@ export default class SystemService {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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.rootPath} && 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 data.tgz`,
|
||||
);
|
||||
return { code: 200, data: res };
|
||||
} catch (error: any) {
|
||||
return { code: 400, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
public async getSystemLog(res: Response) {
|
||||
const result = await readDirs(config.systemLogPath, config.systemLogPath);
|
||||
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+64
-109
@@ -1,33 +1,19 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import {
|
||||
createFile,
|
||||
createRandomString,
|
||||
fileExist,
|
||||
getNetIp,
|
||||
getPlatform,
|
||||
safeJSONParse,
|
||||
} from '../config/util';
|
||||
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { authenticator } from '@otplib/preset-default';
|
||||
import {
|
||||
AuthDataType,
|
||||
SystemInfo,
|
||||
SystemModel,
|
||||
SystemModelInfo,
|
||||
LoginStatus,
|
||||
} from '../data/system';
|
||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import NotificationService from './notify';
|
||||
import { Request } from 'express';
|
||||
import ScheduleService from './schedule';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import dayjs from 'dayjs';
|
||||
import IP2Region from 'ip2region';
|
||||
import requestIp from 'request-ip';
|
||||
import uniq from 'lodash/uniq';
|
||||
|
||||
@Service()
|
||||
export default class UserService {
|
||||
@@ -48,13 +34,12 @@ export default class UserService {
|
||||
req: Request,
|
||||
needTwoFactor = true,
|
||||
): Promise<any> {
|
||||
const _exist = await fileExist(config.authConfigFile);
|
||||
if (!_exist) {
|
||||
if (!fs.existsSync(config.authConfigFile)) {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
|
||||
let { username, password } = payloads;
|
||||
const content = await this.getAuthInfo();
|
||||
const content = this.getAuthInfo();
|
||||
const timestamp = Date.now();
|
||||
if (content) {
|
||||
let {
|
||||
@@ -80,46 +65,32 @@ export default class UserService {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
|
||||
const retriesTime = Math.pow(3, retries) * 1000;
|
||||
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
||||
const waitTime = Math.ceil(
|
||||
(retriesTime - (timestamp - lastlogon)) / 1000,
|
||||
);
|
||||
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
|
||||
return {
|
||||
code: 410,
|
||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||
data: waitTime,
|
||||
message: `失败次数过多,请${Math.round(
|
||||
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||
)}秒后重试`,
|
||||
data: Math.round(
|
||||
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
username === cUsername &&
|
||||
password === cPassword &&
|
||||
twoFactorActivated &&
|
||||
needTwoFactor
|
||||
) {
|
||||
const { ip, address } = await getNetIp(req);
|
||||
if (username === cUsername && password === cPassword) {
|
||||
if (twoFactorActivated && needTwoFactor) {
|
||||
this.updateAuthInfo(content, {
|
||||
isTwoFactorChecking: true,
|
||||
});
|
||||
return {
|
||||
code: 420,
|
||||
message: '',
|
||||
message: '请输入两步验证token',
|
||||
};
|
||||
}
|
||||
|
||||
const ip = requestIp.getClientIp(req) || '';
|
||||
const query = new IP2Region();
|
||||
const ipAddress = query.search(ip);
|
||||
let address = '';
|
||||
if (ipAddress) {
|
||||
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;
|
||||
const expiration = twoFactorActivated ? 30 : 3;
|
||||
let token = jwt.sign({ data }, config.secret as any, {
|
||||
expiresIn: 60 * 60 * 24 * expiration,
|
||||
algorithm: 'HS384',
|
||||
@@ -138,12 +109,13 @@ export default class UserService {
|
||||
platform: req.platform,
|
||||
isTwoFactorChecking: false,
|
||||
});
|
||||
this.notificationService.notify(
|
||||
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: {
|
||||
@@ -166,12 +138,13 @@ export default class UserService {
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
});
|
||||
this.notificationService.notify(
|
||||
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: {
|
||||
@@ -182,40 +155,29 @@ export default class UserService {
|
||||
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> {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const authInfo = this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, {
|
||||
token: '',
|
||||
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||
});
|
||||
}
|
||||
|
||||
public async getLoginLog(): Promise<Array<SystemModelInfo | undefined>> {
|
||||
const docs = await SystemModel.findAll({
|
||||
public async getLoginLog(): Promise<AuthInfo[]> {
|
||||
const docs = await AuthModel.findAll({
|
||||
where: { type: AuthDataType.loginLog },
|
||||
});
|
||||
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) {
|
||||
await SystemModel.destroy({
|
||||
await AuthModel.destroy({
|
||||
where: { id: result[result.length - 1].id },
|
||||
});
|
||||
}
|
||||
@@ -224,22 +186,23 @@ export default class UserService {
|
||||
return [];
|
||||
}
|
||||
|
||||
private async insertDb(payload: SystemInfo): Promise<SystemInfo> {
|
||||
const doc = await SystemModel.create({ ...payload }, { returning: true });
|
||||
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
|
||||
const doc = await AuthModel.create({ ...payload }, { returning: true });
|
||||
return doc;
|
||||
}
|
||||
|
||||
private async initAuthInfo() {
|
||||
await fs.writeFile(
|
||||
private initAuthInfo() {
|
||||
const newPassword = createRandomString(16, 22);
|
||||
fs.writeFileSync(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
password: newPassword,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
code: 100,
|
||||
message: '未找到认证文件,重新初始化',
|
||||
message: '已初始化密码,请前往auth.json查看并重新登录',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,41 +216,36 @@ export default class UserService {
|
||||
if (password === 'admin') {
|
||||
return { code: 400, message: '密码不能设置为admin' };
|
||||
}
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const authInfo = this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, { username, password });
|
||||
return { code: 200, message: '更新成功' };
|
||||
}
|
||||
|
||||
public async updateAvatar(avatar: string) {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const authInfo = this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, { avatar });
|
||||
return { code: 200, data: avatar, message: '更新成功' };
|
||||
}
|
||||
|
||||
public async getUserInfo(): Promise<any> {
|
||||
const authFileExist = await fileExist(config.authConfigFile);
|
||||
if (!authFileExist) {
|
||||
await createFile(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return await this.getAuthInfo();
|
||||
public getUserInfo(): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
||||
if (err) console.log(err);
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async initTwoFactor() {
|
||||
public initTwoFactor() {
|
||||
const secret = authenticator.generateSecret();
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const authInfo = this.getAuthInfo();
|
||||
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
||||
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||
return { secret, url: otpauth };
|
||||
}
|
||||
|
||||
public async activeTwoFactor(code: string) {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
public activeTwoFactor(code: string) {
|
||||
const authInfo = this.getAuthInfo();
|
||||
const isValid = authenticator.verify({
|
||||
token: code,
|
||||
secret: authInfo.twoFactorSecret,
|
||||
@@ -306,7 +264,7 @@ export default class UserService {
|
||||
}: { username: string; password: string; code: string },
|
||||
req: any,
|
||||
) {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const authInfo = this.getAuthInfo();
|
||||
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
|
||||
if (!isTwoFactorChecking) {
|
||||
return { code: 450, message: '未知错误' };
|
||||
@@ -328,8 +286,8 @@ export default class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
public async deactiveTwoFactor() {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
public deactiveTwoFactor() {
|
||||
const authInfo = this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, {
|
||||
twoFactorActivated: false,
|
||||
twoFactorActived: false,
|
||||
@@ -338,13 +296,13 @@ export default class UserService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async getAuthInfo() {
|
||||
const content = await fs.readFile(config.authConfigFile, 'utf8');
|
||||
return safeJSONParse(content);
|
||||
private getAuthInfo() {
|
||||
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
return JSON.parse(content || '{}');
|
||||
}
|
||||
|
||||
private async updateAuthInfo(authInfo: any, info: any) {
|
||||
await fs.writeFile(
|
||||
private updateAuthInfo(authInfo: any, info: any) {
|
||||
fs.writeFileSync(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({ ...authInfo, ...info }),
|
||||
);
|
||||
@@ -352,29 +310,26 @@ export default class UserService {
|
||||
|
||||
public async getNotificationMode(): Promise<NotificationInfo> {
|
||||
const doc = await this.getDb({ type: AuthDataType.notification });
|
||||
return (doc.info || {}) as NotificationInfo;
|
||||
return (doc && doc.info) || {};
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: SystemInfo): Promise<any> {
|
||||
let doc = await SystemModel.findOne({ type: payload.type });
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
let doc = await AuthModel.findOne({ type: payload.type });
|
||||
if (doc) {
|
||||
const updateResult = await SystemModel.update(payload, {
|
||||
const updateResult = await AuthModel.update(payload, {
|
||||
where: { id: doc.id },
|
||||
returning: true,
|
||||
});
|
||||
doc = updateResult[1][0];
|
||||
} else {
|
||||
doc = await SystemModel.create(payload, { returning: true });
|
||||
doc = await AuthModel.create(payload, { returning: true });
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<SystemInfo> {
|
||||
const doc = await SystemModel.findOne({ where: { ...query } });
|
||||
if (!doc) {
|
||||
throw new Error(`${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
public async getDb(query: any): Promise<any> {
|
||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as any);
|
||||
}
|
||||
|
||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
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';
|
||||
|
||||
interface IDependencyFn<T> {
|
||||
(): Promise<T>;
|
||||
dependency?: Dependence;
|
||||
}
|
||||
class TaskLimit {
|
||||
private dependenyLimit = new PQueue({ concurrency: 1 });
|
||||
private queuedDependencyIds = new Set<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),
|
||||
});
|
||||
|
||||
get cronLimitActiveCount() {
|
||||
return this.cronLimit.pending;
|
||||
}
|
||||
|
||||
get cronLimitPendingCount() {
|
||||
return this.cronLimit.size;
|
||||
}
|
||||
|
||||
get firstDependencyId() {
|
||||
return [...this.queuedDependencyIds.values()][0];
|
||||
}
|
||||
|
||||
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 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>(
|
||||
fn: () => Promise<T>,
|
||||
options?: Partial<QueueAddOptions>,
|
||||
): Promise<T | void> {
|
||||
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 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();
|
||||
@@ -1,31 +0,0 @@
|
||||
import { spawn } from 'cross-spawn';
|
||||
import taskLimit from './pLimit';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
export function runCron(cmd: string, options?: { schedule: string; extraSchedules: Array<{ schedule: string }>; name: string }): Promise<number | void> {
|
||||
return taskLimit.runWithCronLimit(() => {
|
||||
return new Promise(async (resolve: any) => {
|
||||
Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, 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) => {
|
||||
resolve({ ...options, command: cmd, pid: cp.pid, code });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'reflect-metadata';
|
||||
import OpenService from './services/open';
|
||||
import { Container } from 'typedi';
|
||||
import LoggerInstance from './loaders/logger';
|
||||
import fs from 'fs';
|
||||
import config from './config';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
const tokenFile = path.join(config.configPath, 'token.json');
|
||||
|
||||
async function getToken() {
|
||||
try {
|
||||
Container.set('logger', LoggerInstance);
|
||||
const openService = Container.get(OpenService);
|
||||
const appToken = await openService.generateSystemToken();
|
||||
console.log(appToken.value);
|
||||
await writeFile({
|
||||
value: appToken.value,
|
||||
expiration: appToken.expiration,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeFile(data: any) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.writeFile(
|
||||
tokenFile,
|
||||
`${JSON.stringify(data)}${os.EOL}`,
|
||||
{ encoding: 'utf8' },
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getToken();
|
||||
@@ -1,27 +0,0 @@
|
||||
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();
|
||||
-274
@@ -1,274 +0,0 @@
|
||||
#!/usr/bin/env zx
|
||||
import path from 'path';
|
||||
|
||||
const dir_root = process.env.QL_DIR;
|
||||
const file_auth_token = path.join(dir_root, 'static/auth.json');
|
||||
const token_file = path.join(dir_root, 'static/build/token.js');
|
||||
|
||||
let token;
|
||||
|
||||
const createToken = async () => {
|
||||
let tokenCommand = `tsx ${dir_root}/back/token.ts`;
|
||||
if (await fs.exists(token_file)) {
|
||||
tokenCommand = `node ${token_file}`;
|
||||
}
|
||||
token = (await $([tokenCommand])).stdout.trim();
|
||||
};
|
||||
|
||||
const getToken = async () => {
|
||||
if (fs.existsSync(file_auth_token)) {
|
||||
const authTokenData = JSON.parse(fs.readFileSync(file_auth_token, 'utf8'));
|
||||
token = authTokenData.value;
|
||||
const expiration = authTokenData.expiration;
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
if (currentTimeStamp >= expiration) {
|
||||
await createToken();
|
||||
}
|
||||
} else {
|
||||
await createToken();
|
||||
}
|
||||
};
|
||||
|
||||
export const addCronApi = async (schedule, command, name, subId = null) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
name,
|
||||
command,
|
||||
schedule,
|
||||
sub_id: subId,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log(`${name} -> 添加成功`);
|
||||
} else {
|
||||
console.log(`${name} -> 添加失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${name} -> 添加失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCronApi = async (schedule, command, name, id) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
name,
|
||||
command,
|
||||
schedule,
|
||||
id,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log(`${name} -> 更新成功`);
|
||||
} else {
|
||||
console.log(`${name} -> 更新失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${name} -> 更新失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCronCommandApi = async (command, id) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
command,
|
||||
id,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log(`${command} -> 更新成功`);
|
||||
} else {
|
||||
console.log(`${command} -> 更新失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${command} -> 更新失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const delCronApi = async (ids) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(ids),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log('成功');
|
||||
} else {
|
||||
console.log(`失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`删除失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCron = async (
|
||||
ids,
|
||||
status,
|
||||
pid,
|
||||
logPath,
|
||||
lastExecutingTime = 0,
|
||||
runningTime = 0,
|
||||
) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
ids,
|
||||
status,
|
||||
pid,
|
||||
log_path: logPath,
|
||||
last_execution_time: lastExecutingTime,
|
||||
last_running_time: runningTime,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons/status?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code !== 200) {
|
||||
console.log(`更新任务状态失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`更新任务状态失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const notifyApi = async (title, content) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
title,
|
||||
content,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/system/notify?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log('通知发送成功🎉');
|
||||
} else {
|
||||
console.log(`通知失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`通知失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const findCronApi = async (params) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons/detail?${params}&t=${currentTimeStamp}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { data } = responseData;
|
||||
if (data === 'null') {
|
||||
console.log('');
|
||||
} else {
|
||||
const { name } = data;
|
||||
console.log(name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`查找失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
await getToken();
|
||||
@@ -0,0 +1,13 @@
|
||||
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) => {},
|
||||
};
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
import 'zx/globals';
|
||||
let initialVars = [];
|
||||
|
||||
export const storeEnvVars = async () => {
|
||||
const stdout = (await $`env`).lines();
|
||||
initialVars = stdout.map((line) => line.split('=')[0]);
|
||||
};
|
||||
|
||||
export const restoreEnvVars = async () => {
|
||||
const stdout = (await $`env`).lines();
|
||||
const currentVars = stdout.map((line) => line.split('=')[0]);
|
||||
|
||||
for (const key of currentVars) {
|
||||
if (!initialVars.includes(key)) {
|
||||
await $`unset ${key}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await storeEnvVars();
|
||||
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
-302
@@ -1,302 +0,0 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
import {
|
||||
dirScripts,
|
||||
dirLog,
|
||||
handleTaskStart,
|
||||
runTaskBefore,
|
||||
runTaskAfter,
|
||||
handleTaskEnd,
|
||||
globalState,
|
||||
formatLogTime,
|
||||
} from './share.mjs';
|
||||
import { basename, dirname } from 'path';
|
||||
|
||||
function defineProgram(fileParam) {
|
||||
if (fileParam.endsWith('.js') || fileParam.endsWith('.mjs')) {
|
||||
return 'node';
|
||||
} else if (fileParam.endsWith('.py') || fileParam.endsWith('.pyc')) {
|
||||
return 'python3';
|
||||
} else if (fileParam.endsWith('.sh')) {
|
||||
return 'bash';
|
||||
} else if (fileParam.endsWith('.ts')) {
|
||||
return $`command -v tsx`
|
||||
.then(() => 'tsx')
|
||||
.catch(() => 'ts-node-transpile-only');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function randomDelay(fileParam) {
|
||||
const randomDelayMax = process.env.RandomDelay;
|
||||
if (randomDelayMax && randomDelayMax > 0) {
|
||||
const fileExtensions = process.env.RandomDelayFileExtensions || 'js';
|
||||
const ignoredMinutes = process.env.RandomDelayIgnoredMinutes || '0 30';
|
||||
const currentMin = new Date().getMinutes();
|
||||
|
||||
if (
|
||||
fileExtensions.split(' ').some((ext) => fileParam.endsWith(`.${ext}`))
|
||||
) {
|
||||
if (
|
||||
ignoredMinutes.split(' ').some((min) => parseInt(min) === currentMin)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const delaySecond = Math.floor(Math.random() * randomDelayMax) + 1;
|
||||
console.log(
|
||||
`任务随机延迟 ${delaySecond} 秒,配置文件参数 RandomDelay 置空可取消延迟`,
|
||||
);
|
||||
await sleep(delaySecond * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function genArrayScripts() {
|
||||
const arrayScripts = [];
|
||||
const arrayScriptsName = [];
|
||||
const files = await fs.readdir(dirScripts);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.js') && file !== 'sendNotify.js') {
|
||||
arrayScripts.push(file);
|
||||
const content = await fs.readFile(`${dirScripts}/${file}`, 'utf8');
|
||||
const match = content.match(/new Env\(['"]([^'"]+)['"]\)/);
|
||||
arrayScriptsName.push(match ? match[1] : '<未识别出活动名称>');
|
||||
}
|
||||
}
|
||||
|
||||
return { arrayScripts, arrayScriptsName };
|
||||
}
|
||||
|
||||
async function usage() {
|
||||
const { arrayScripts, arrayScriptsName } = await genArrayScripts();
|
||||
console.log(
|
||||
`task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “${dirScripts}/” 目录后的相对路径(定时任务中请写作相对路径),用法为:`,
|
||||
);
|
||||
console.log(
|
||||
`1.$cmdTask <fileName> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数`,
|
||||
);
|
||||
console.log(
|
||||
`2.$cmdTask <fileName> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中`,
|
||||
);
|
||||
console.log(
|
||||
`3.$cmdTask <fileName> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行`,
|
||||
);
|
||||
console.log(
|
||||
`4.$cmdTask <fileName> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行`,
|
||||
);
|
||||
if (arrayScripts.length > 0) {
|
||||
console.log(`\n当前有以下脚本可以运行:`);
|
||||
arrayScripts.forEach((script, i) =>
|
||||
console.log(`${i + 1}. ${arrayScriptsName[i]}:${script}`),
|
||||
);
|
||||
} else {
|
||||
console.log(`\n暂无脚本可以执行`);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseDuration(d) {
|
||||
if (typeof d == 'number') {
|
||||
if (isNaN(d) || d < 0) throw new Error(`Invalid duration: "${d}".`);
|
||||
return d;
|
||||
} else if (/\d+s/.test(d)) {
|
||||
return +d.slice(0, -1) * 1000;
|
||||
} else if (/\d+ms/.test(d)) {
|
||||
return +d.slice(0, -2);
|
||||
} else if (/\d+m/.test(d)) {
|
||||
return +d.slice(0, -1) * 1000 * 60;
|
||||
} else if (/\d+h/.test(d)) {
|
||||
return +d.slice(0, -1) * 1000 * 60 * 60;
|
||||
} else if (/\d+d/.test(d)) {
|
||||
return +d.slice(0, -1) * 1000 * 60 * 60 * 24;
|
||||
}
|
||||
throw new Error(`Unknown duration: "${d}".`);
|
||||
}
|
||||
|
||||
async function runWithTimeout(command) {
|
||||
if (globalState.commandTimeoutTime) {
|
||||
const timeoutNumber = parseDuration(globalState.commandTimeoutTime);
|
||||
await $([command]).timeout(timeoutNumber).nothrow().pipe(process.stdout);
|
||||
} else {
|
||||
await $([command]).nothrow().pipe(process.stdout);
|
||||
}
|
||||
}
|
||||
|
||||
async function runNormal(fileParam, scriptParams) {
|
||||
if (
|
||||
!scriptParams.includes('now') &&
|
||||
process.env.realTime !== 'true' &&
|
||||
process.env.noDelay !== 'true'
|
||||
) {
|
||||
await randomDelay(fileParam);
|
||||
}
|
||||
cd(dirScripts);
|
||||
const relativePath = dirname(fileParam);
|
||||
if (!fileParam.startsWith('/') && relativePath) {
|
||||
cd(relativePath);
|
||||
fileParam = fileParam.replace(`${relativePath}/`, '');
|
||||
}
|
||||
await runWithTimeout(
|
||||
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function runConcurrent(fileParam, envParam, numParam, scriptParams) {
|
||||
if (!envParam || !numParam) {
|
||||
console.log(`缺少并发运行的环境变量参数 task xxx.js conc Test 1 3`);
|
||||
return;
|
||||
}
|
||||
|
||||
const array = (process.env[envParam] || '').split('&');
|
||||
const runArr = expandRange(numParam, array.length);
|
||||
const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean);
|
||||
|
||||
const singleLogTime = formatLogTime(new Date());
|
||||
|
||||
cd(dirScripts);
|
||||
const relativePath = dirname(fileParam);
|
||||
if (relativePath && fileParam.includes('/')) {
|
||||
cd(relativePath);
|
||||
fileParam = fileParam.replace(`${relativePath}/`, '');
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
arrayRun.map(async (env, i) => {
|
||||
const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${
|
||||
i + 1
|
||||
}.log`;
|
||||
await runWithTimeout(
|
||||
`${envParam}="${env.replace('"', '\\"')}" ${
|
||||
globalState.whichProgram
|
||||
} ${fileParam} ${scriptParams} &>${singleLogPath}`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
for (let i = 0; i < arrayRun.length; i++) {
|
||||
const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${
|
||||
i + 1
|
||||
}.log`;
|
||||
const log = await fs.readFile(singleLogPath, 'utf8');
|
||||
console.log(log);
|
||||
await fs.unlink(singleLogPath);
|
||||
}
|
||||
}
|
||||
|
||||
async function runDesignated(fileParam, envParam, numParam, scriptParams) {
|
||||
if (!envParam || !numParam) {
|
||||
console.log(`缺少单独运行的参数 task xxx.js desi Test 1 3`);
|
||||
return;
|
||||
}
|
||||
|
||||
const array = (process.env[envParam] || '').split('&');
|
||||
const runArr = expandRange(numParam, array.length);
|
||||
const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean);
|
||||
const cookieStr = arrayRun.join('&');
|
||||
cd(dirScripts);
|
||||
const relativePath = dirname(fileParam);
|
||||
if (relativePath && fileParam.includes('/')) {
|
||||
cd(relativePath);
|
||||
fileParam = fileParam.replace(`${relativePath}/`, '');
|
||||
}
|
||||
|
||||
console.log('cookieStr', cookieStr.length, arrayRun.length);
|
||||
// ${envParam}="${cookieStr.replace('"', '\\"')}"
|
||||
await runWithTimeout(
|
||||
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function runElse(fileParam, scriptParams) {
|
||||
cd(dirScripts);
|
||||
const relativePath = dirname(fileParam);
|
||||
if (relativePath && fileParam.includes('/')) {
|
||||
cd(relativePath);
|
||||
fileParam = fileParam.replace(`${relativePath}/`, './');
|
||||
}
|
||||
|
||||
await runWithTimeout(
|
||||
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
|
||||
);
|
||||
}
|
||||
|
||||
function expandRange(rangeStr, max) {
|
||||
const tempRangeStr = rangeStr
|
||||
.replace(/-max/g, `-${max}`)
|
||||
.replace(/max-/g, `${max}-`);
|
||||
|
||||
return tempRangeStr.split(' ').flatMap((part) => {
|
||||
const rangeMatch = part.match(/^(\d+)([-~_])(\d+)$/);
|
||||
if (rangeMatch) {
|
||||
const [, start, , end] = rangeMatch.map(Number);
|
||||
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||
}
|
||||
return Number(part);
|
||||
});
|
||||
}
|
||||
|
||||
async function main(taskShellParams, scriptParams) {
|
||||
const [fileParam, action, envParam, ...others] = taskShellParams;
|
||||
if (taskShellParams.length === 0) {
|
||||
return await usage();
|
||||
}
|
||||
if (fileParam && /\.(js|py|pyc|sh|ts)$/.test(fileParam)) {
|
||||
const filePath = fileParam.startsWith('/')
|
||||
? fileParam
|
||||
: path.join(dirScripts, fileParam);
|
||||
if (!(await fs.exists(filePath))) {
|
||||
console.log(`文件不存在 ${fileParam}`);
|
||||
return;
|
||||
}
|
||||
switch (action) {
|
||||
case undefined:
|
||||
return await runNormal(fileParam, scriptParams);
|
||||
case 'now':
|
||||
return await runNormal(fileParam, scriptParams);
|
||||
case 'conc':
|
||||
return await runConcurrent(
|
||||
fileParam,
|
||||
envParam,
|
||||
others.join(' '),
|
||||
scriptParams,
|
||||
);
|
||||
case 'desi':
|
||||
return await runDesignated(
|
||||
fileParam,
|
||||
envParam,
|
||||
others.join(' '),
|
||||
scriptParams,
|
||||
);
|
||||
}
|
||||
}
|
||||
await runElse(fileParam, taskShellParams.slice(1).concat(scriptParams));
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const taskArgv = minimist(process.argv.slice(3), {
|
||||
'--': true,
|
||||
});
|
||||
const { _: taskShellParams, m, '--': scriptParams, GlobalState } = taskArgv;
|
||||
const cacheState = JSON.parse(GlobalState || '{}');
|
||||
for (const key in cacheState) {
|
||||
globalState[key] = cacheState[key];
|
||||
}
|
||||
if (m) {
|
||||
globalState.commandTimeoutTime = m;
|
||||
}
|
||||
globalState.whichProgram = await defineProgram(taskShellParams[0]);
|
||||
|
||||
await handleTaskStart();
|
||||
await runTaskBefore();
|
||||
await main(taskShellParams, scriptParams);
|
||||
await runTaskAfter();
|
||||
await handleTaskEnd();
|
||||
}
|
||||
|
||||
async function singleHandle() {}
|
||||
|
||||
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP'];
|
||||
signals.forEach((sig) => process.on(sig, singleHandle));
|
||||
|
||||
await run();
|
||||
-500
@@ -1,500 +0,0 @@
|
||||
#!/usr/bin/env zx
|
||||
import 'zx/globals';
|
||||
// $.verbose = true;
|
||||
|
||||
import { updateCron, notifyApi } from './api.mjs';
|
||||
import { restoreEnvVars } from './env.mjs';
|
||||
|
||||
export const dirRoot = process.env.QL_DIR;
|
||||
export const dirTmp = path.join(dirRoot, '.tmp');
|
||||
export const dirData = process.env.QL_DATA_DIR
|
||||
? process.env.QL_DATA_DIR.endsWith('/')
|
||||
? process.env.QL_DATA_DIR.slice(-1)
|
||||
: process.env.QL_DATA_DIR
|
||||
: path.join(dirRoot, 'data');
|
||||
export const dirShell = path.join(dirRoot, 'shell');
|
||||
export const dirCli = path.join(dirRoot, 'cli');
|
||||
export const dirSample = path.join(dirRoot, 'sample');
|
||||
export const dirStatic = path.join(dirRoot, 'static');
|
||||
export const dirConfig = path.join(dirData, 'config');
|
||||
export const dirScripts = path.join(dirData, 'scripts');
|
||||
export const dirRepo = path.join(dirData, 'repo');
|
||||
export const dirRaw = path.join(dirData, 'raw');
|
||||
export const dirLog = path.join(dirData, 'log');
|
||||
export const dirDb = path.join(dirData, 'db');
|
||||
export const dirDep = path.join(dirData, 'deps');
|
||||
export const dirListTmp = path.join(dirLog, '.tmp');
|
||||
export const dirUpdateLog = path.join(dirLog, 'update');
|
||||
export const qlStaticRepo = path.join(dirRepo, 'static');
|
||||
|
||||
export const fileConfigSample = path.join(dirSample, 'config.sample.sh');
|
||||
export const fileEnv = path.join(dirConfig, 'env.sh');
|
||||
export const jsFileEnv = path.join(dirConfig, 'env.js');
|
||||
export const fileConfigUser = path.join(dirConfig, 'config.sh');
|
||||
export const fileAuthSample = path.join(dirSample, 'auth.sample.json');
|
||||
export const fileAuthUser = path.join(dirConfig, 'auth.json');
|
||||
export const fileAuthToken = path.join(dirConfig, 'token.json');
|
||||
export const fileExtraShell = path.join(dirConfig, 'extra.sh');
|
||||
export const fileTaskBefore = path.join(dirConfig, 'task_before.sh');
|
||||
export const fileTaskAfter = path.join(dirConfig, 'task_after.sh');
|
||||
export const fileTaskSample = path.join(dirSample, 'task.sample.sh');
|
||||
export const fileExtraSample = path.join(dirSample, 'extra.sample.sh');
|
||||
export const fileNotifyJsSample = path.join(dirSample, 'notify.js');
|
||||
export const fileNotifyPySample = path.join(dirSample, 'notify.py');
|
||||
export const fileTestJsSample = path.join(dirSample, 'ql_sample.js');
|
||||
export const fileTestPySample = path.join(dirSample, 'ql_sample.py');
|
||||
export const fileNotifyPy = path.join(dirScripts, 'notify.py');
|
||||
export const fileNotifyJs = path.join(dirScripts, 'sendNotify.js');
|
||||
export const fileTestJs = path.join(dirScripts, 'ql_sample.js');
|
||||
export const fileTestPy = path.join(dirScripts, 'ql_sample.py');
|
||||
export const nginxAppConf = path.join(dirRoot, 'docker/front.conf');
|
||||
export const nginxConf = path.join(dirRoot, 'docker/nginx.conf');
|
||||
export const depNotifyPy = path.join(dirDep, 'notify.py');
|
||||
export const depNotifyJs = path.join(dirDep, 'sendNotify.js');
|
||||
|
||||
export const listCrontabUser = path.join(dirConfig, 'crontab.list');
|
||||
export const listCrontabSample = path.join(dirSample, 'crontab.sample.list');
|
||||
export const listOwnScripts = path.join(dirListTmp, 'own_scripts.list');
|
||||
export const listOwnUser = path.join(dirListTmp, 'own_user.list');
|
||||
export const listOwnAdd = path.join(dirListTmp, 'own_add.list');
|
||||
export const listOwnDrop = path.join(dirListTmp, 'own_drop.list');
|
||||
|
||||
export const globalState = {};
|
||||
|
||||
export const initEnv = () => {
|
||||
$.prefix +=
|
||||
'export 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;';
|
||||
$.prefix += 'export PYTHONUNBUFFERED=1;';
|
||||
$.prefix += 'export TERM=xterm-color;';
|
||||
};
|
||||
|
||||
export const importConfig = async () => {
|
||||
if (await fs.exists(fileConfigUser)) {
|
||||
$.prefix += (await fs.readFile(fileConfigUser, 'utf8'));
|
||||
}
|
||||
// if (process.env.LOAD_ENV !== 'false' && (await fs.exists(fileEnv))) {
|
||||
// $.prefix += (await fs.readFile(fileEnv, 'utf8'));
|
||||
// }
|
||||
require(jsFileEnv)
|
||||
|
||||
globalState.qlBaseUrl = process.env.QlBaseUrl || '/';
|
||||
globalState.qlPort = process.env.QlPort || '5700';
|
||||
globalState.commandTimeoutTime = process.env.CommandTimeoutTime;
|
||||
globalState.fileExtensions = process.env.RepoFileExtensions || 'js py';
|
||||
globalState.proxyUrl = process.env.ProxyUrl || '';
|
||||
globalState.currentBranch = process.env.QL_BRANCH;
|
||||
|
||||
if (process.env.DefaultCronRule) {
|
||||
globalState.defaultCron = process.env.DefaultCronRule;
|
||||
} else {
|
||||
globalState.defaultCron = `${Math.floor(Math.random() * 60)} ${Math.floor(
|
||||
Math.random() * 24,
|
||||
)} * * *`;
|
||||
}
|
||||
|
||||
globalState.cpuWarn = process.env.CpuWarn;
|
||||
globalState.memWarn = process.env.MemoryWarn;
|
||||
globalState.diskWarn = process.env.DiskWarn;
|
||||
};
|
||||
|
||||
export const setProxy = (proxy) => {
|
||||
if (proxy) {
|
||||
globalState.proxyUrl = proxy;
|
||||
}
|
||||
if (globalState.proxyUrl) {
|
||||
$`export http_proxy=${globalState.proxyUrl}`;
|
||||
$`export https_proxy=${globalState.proxyUrl}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const unsetProxy = () => {
|
||||
$`unset http_proxy`;
|
||||
$`unset https_proxy`;
|
||||
};
|
||||
|
||||
export const makeDir = async (dir) => {
|
||||
if (!(await fs.exists(dir))) {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
export const detectTermux = () => {
|
||||
globalState.isTermux = process.env.PATH?.includes('com.termux') ? 1 : 0;
|
||||
};
|
||||
|
||||
export const detectMacos = () => {
|
||||
globalState.isMacos = os.type() === 'Darwin' ? 1 : 0;
|
||||
};
|
||||
|
||||
export const genRandomNum = (number) => {
|
||||
return Math.floor(Math.random() * number);
|
||||
};
|
||||
|
||||
export const fixConfig = async () => {
|
||||
await makeDir(dirTmp);
|
||||
await makeDir(dirStatic);
|
||||
await makeDir(dirData);
|
||||
await makeDir(dirConfig);
|
||||
await makeDir(dirLog);
|
||||
await makeDir(dirDb);
|
||||
await makeDir(dirScripts);
|
||||
await makeDir(dirListTmp);
|
||||
await makeDir(dirRepo);
|
||||
await makeDir(dirRaw);
|
||||
await makeDir(dirUpdateLog);
|
||||
await makeDir(dirDep);
|
||||
|
||||
if (!(await fs.exists(fileConfigUser))) {
|
||||
console.log(
|
||||
`复制一份 ${fileConfigSample} 为 ${fileConfigUser},随后请按注释编辑你的配置文件:${fileConfigUser}`,
|
||||
);
|
||||
await fs.copyFile(fileConfigSample, fileConfigUser);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileEnv))) {
|
||||
console.log(
|
||||
'检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...',
|
||||
);
|
||||
await fs.writeFile(fileEnv, '');
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileTaskBefore))) {
|
||||
console.log(`复制一份 ${fileTaskSample} 为 ${fileTaskBefore}`);
|
||||
await fs.copyFile(fileTaskSample, fileTaskBefore);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileTaskAfter))) {
|
||||
console.log(`复制一份 ${fileTaskSample} 为 ${fileTaskAfter}`);
|
||||
await fs.copyFile(fileTaskSample, fileTaskAfter);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileExtraShell))) {
|
||||
console.log(`复制一份 ${fileExtraSample} 为 ${fileExtraShell}`);
|
||||
await fs.copyFile(fileExtraSample, fileExtraShell);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileAuthUser))) {
|
||||
console.log(`复制一份 ${fileAuthSample} 为 ${fileAuthUser}`);
|
||||
await fs.copyFile(fileAuthSample, fileAuthUser);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileNotifyPy))) {
|
||||
console.log(`复制一份 ${fileNotifyPySample} 为 ${fileNotifyPy}`);
|
||||
await fs.copyFile(fileNotifyPySample, fileNotifyPy);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileNotifyJs))) {
|
||||
console.log(`复制一份 ${fileNotifyJsSample} 为 ${fileNotifyJs}`);
|
||||
await fs.copyFile(fileNotifyJsSample, fileNotifyJs);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileTestJs))) {
|
||||
await fs.copyFile(fileTestJsSample, fileTestJs);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileTestPy))) {
|
||||
await fs.copyFile(fileTestPySample, fileTestPy);
|
||||
}
|
||||
|
||||
if (await fs.exists('/etc/nginx/conf.d/default.conf')) {
|
||||
console.log('检测到你可能未修改过默认nginx配置,将帮你删除');
|
||||
await fs.unlink('/etc/nginx/conf.d/default.conf');
|
||||
}
|
||||
if (!(await fs.exists(depNotifyJs))) {
|
||||
console.log(`复制一份 ${fileNotifyJsSample} 为 ${depNotifyJs}`);
|
||||
await fs.copyFile(fileNotifyJsSample, depNotifyJs);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(depNotifyPy))) {
|
||||
console.log(`复制一份 ${fileNotifyPySample} 为 ${depNotifyPy}`);
|
||||
await fs.copyFile(fileNotifyPySample, depNotifyPy);
|
||||
}
|
||||
};
|
||||
|
||||
export const npmInstallSub = async () => {
|
||||
if (globalState.isTermux === 1) {
|
||||
await $`npm install --production --no-bin-links`;
|
||||
} else if (!(await $`command -v pnpm`)) {
|
||||
await $`npm install --production`;
|
||||
} else {
|
||||
await $`pnpm install --loglevel error --production`;
|
||||
}
|
||||
};
|
||||
|
||||
export const npmInstall = async (dirWork) => {
|
||||
const dirCurrent = process.cwd();
|
||||
|
||||
await $`cd ${dirWork}`;
|
||||
console.log(`安装 ${dirWork} 依赖包...`);
|
||||
await npmInstallSub();
|
||||
await $`cd ${dirCurrent}`;
|
||||
};
|
||||
|
||||
export const diffAndCopy = async (copySource, copyTo) => {
|
||||
if (
|
||||
!(await fs.exists(copyTo)) ||
|
||||
(await $`diff ${copySource} ${copyTo}`).exitCode !== 0
|
||||
) {
|
||||
await fs.copyFile(copySource, copyTo);
|
||||
}
|
||||
};
|
||||
|
||||
export const gitCloneScripts = async (url, dir, branch, proxy) => {
|
||||
const partCmd = branch ? `-b ${branch}` : '';
|
||||
console.log(`开始拉取仓库 ${globalState.uniqPath} 到 ${dir}`);
|
||||
|
||||
setProxy(proxy);
|
||||
|
||||
const res = await $`git clone -q --depth=1 ${partCmd} ${url} ${dir}`;
|
||||
globalState.exitStatus = res.exitCode;
|
||||
|
||||
unsetProxy();
|
||||
};
|
||||
|
||||
export const randomRange = (begin, end) => {
|
||||
return Math.floor(Math.random() * (end - begin) + begin);
|
||||
};
|
||||
|
||||
export const deletePm2 = async () => {
|
||||
await $`cd ${dirRoot}`;
|
||||
await $`pm2 delete ecosystem.config.js`;
|
||||
};
|
||||
|
||||
export const reloadPm2 = async () => {
|
||||
await $`cd ${dirRoot}`;
|
||||
restoreEnvVars();
|
||||
await $`pm2 flush &>/dev/null`;
|
||||
await $`pm2 startOrGracefulReload ecosystem.config.js`;
|
||||
};
|
||||
|
||||
export const reloadUpdate = async () => {
|
||||
await $`cd ${dirRoot}`;
|
||||
restoreEnvVars();
|
||||
await $`pm2 flush &>/dev/null`;
|
||||
await $`pm2 startOrGracefulReload other.config.js`;
|
||||
};
|
||||
|
||||
export const diffTime = (beginTime, endTime) => {
|
||||
let diffTime;
|
||||
if (globalState.isMacos === 1) {
|
||||
diffTime = (+new Date(endTime) - +new Date(beginTime)) / 1000;
|
||||
} else {
|
||||
diffTime =
|
||||
(new Date(endTime).getTime() - new Date(beginTime).getTime()) / 1000;
|
||||
}
|
||||
return diffTime;
|
||||
};
|
||||
|
||||
export const formatTime = (time) => {
|
||||
// 秒
|
||||
return new Date(time).toLocaleString();
|
||||
};
|
||||
|
||||
function pad(n, min = 10) {
|
||||
return n < min ? '0' + n : n;
|
||||
}
|
||||
|
||||
export function formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const day = pad(date.getDate());
|
||||
const hour = pad(date.getHours());
|
||||
const minute = pad(date.getMinutes());
|
||||
const second = pad(date.getSeconds());
|
||||
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
export const formatLogTime = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const day = pad(date.getDate());
|
||||
const hour = pad(date.getHours());
|
||||
const minute = pad(date.getMinutes());
|
||||
const second = pad(date.getSeconds());
|
||||
const milliSecond = pad(date.getMilliseconds(), 100);
|
||||
|
||||
return `${year}-${month}-${day}-${hour}-${minute}-${second}-${milliSecond}`;
|
||||
};
|
||||
|
||||
export const formatTimestamp = (date) => {
|
||||
return Math.floor(date.getTime() / 1000);
|
||||
};
|
||||
|
||||
export const patchVersion = async () => {
|
||||
await $`git config --global pull.rebase false`;
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'db/cookie.db'))) {
|
||||
console.log('检测到旧的db文件,拷贝为新db...');
|
||||
await $`mv ${path.join(dirRoot, 'db/cookie.db')} ${path.join(
|
||||
dirRoot,
|
||||
'db/env.db',
|
||||
)}`;
|
||||
await $`rm -rf ${path.join(dirRoot, 'db/cookie.db')}`;
|
||||
}
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'db'))) {
|
||||
console.log('检测到旧的db目录,拷贝到data目录...');
|
||||
await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`;
|
||||
}
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'scripts'))) {
|
||||
console.log('检测到旧的scripts目录,拷贝到data目录...');
|
||||
await $`cp -rf ${path.join(dirRoot, 'scripts')} ${dirData}`;
|
||||
}
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'log'))) {
|
||||
console.log('检测到旧的log目录,拷贝到data目录...');
|
||||
await $`cp -rf ${path.join(dirRoot, 'log')} ${dirData}`;
|
||||
}
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'config'))) {
|
||||
console.log('检测到旧的config目录,拷贝到data目录...');
|
||||
await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const initNginx = async () => {
|
||||
await fs.copyFile(nginxConf, '/etc/nginx/nginx.conf');
|
||||
await fs.copyFile(nginxAppConf, '/etc/nginx/conf.d/front.conf');
|
||||
let locationUrl = '/';
|
||||
let aliasStr = '';
|
||||
let rootStr = '';
|
||||
let qlBaseUrl = globalState.qlBaseUrl;
|
||||
let qlPort = globalState.qlPort;
|
||||
if (qlBaseUrl !== '/') {
|
||||
if (!qlBaseUrl.startsWith('/')) {
|
||||
qlBaseUrl = `/${qlBaseUrl}`;
|
||||
}
|
||||
if (!qlBaseUrl.endsWith('/')) {
|
||||
qlBaseUrl = `${qlBaseUrl}/`;
|
||||
}
|
||||
locationUrl = `^~${qlBaseUrl.slice(0, -1)}`;
|
||||
aliasStr = `alias ${path.join(dirStatic, 'dist')};`;
|
||||
const file = await fs.readFile(
|
||||
path.join(dirStatic, 'dist/index.html'),
|
||||
'utf8',
|
||||
);
|
||||
if (!file.includes(`<base href="${qlBaseUrl}">`)) {
|
||||
await fs.writeFile(
|
||||
path.join(dirStatic, 'dist/index.html'),
|
||||
`<base href="${qlBaseUrl}">\n${file}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
rootStr = `root ${path.join(dirStatic, 'dist')};`;
|
||||
}
|
||||
await $`sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf`;
|
||||
await $`sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf`;
|
||||
await $`sed -i "s,QL_BASE_URL_LOCATION,${locationUrl},g" /etc/nginx/conf.d/front.conf`;
|
||||
let ipv6Str = '';
|
||||
const ipv6 = await $`ip a | grep inet6`;
|
||||
if (ipv6.stdout.trim()) {
|
||||
ipv6Str = 'listen [::]:${qlPort} ipv6only=on;';
|
||||
}
|
||||
const ipv4Str = `listen ${qlPort};`;
|
||||
await $`sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf`;
|
||||
await $`sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf`;
|
||||
};
|
||||
|
||||
async function checkServer() {
|
||||
const cpuWarn = parseInt(process.env.cpuWarn || '0');
|
||||
const memWarn = parseInt(process.env.memWarn || '0');
|
||||
const diskWarn = parseInt(process.env.diskWarn || '0');
|
||||
|
||||
if (cpuWarn && memWarn && diskWarn) {
|
||||
const topResult = await $`top -b -n 1`;
|
||||
const cpuUse = parseInt(
|
||||
topResult.stdout.match(/CPU\s+(\d+)\%/)?.[1] || '0',
|
||||
);
|
||||
const memFree = parseInt(
|
||||
(await $`free -m`).stdout.match(/Mem:\s+(\d+)/)?.[1] || '0',
|
||||
);
|
||||
const memTotal = parseInt(
|
||||
(await $`free -m`).stdout.match(/Mem:\s+\d+\s+(\d+)/)?.[1] || '0',
|
||||
);
|
||||
const diskUse = parseInt(
|
||||
(await $`df -P`).stdout.match(/\/dev.*\s+(\d+)\%/)?.[1] || '0',
|
||||
);
|
||||
if (memFree && memTotal && diskUse && cpuUse) {
|
||||
const memUse = Math.floor((memFree * 100) / memTotal);
|
||||
|
||||
if (cpuUse > cpuWarn || memFree < memWarn || diskUse > diskWarn) {
|
||||
const resource = topResult.stdout
|
||||
.split('\n')
|
||||
.slice(7, 17)
|
||||
.map((line) => line.replace(/\s+/g, ' '))
|
||||
.join('\\n');
|
||||
await notifyApi(
|
||||
'服务器资源异常警告',
|
||||
`当前CPU占用 ${cpuUse}% 内存占用 ${memUse}% 磁盘占用 ${diskUse}% \n资源占用详情 \n\n ${resource}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const handleTaskStart = async () => {
|
||||
if (globalState.ID) {
|
||||
await updateCron(
|
||||
[globalState.ID],
|
||||
'0',
|
||||
String(process.pid),
|
||||
globalState.logPath,
|
||||
globalState.beginTimestamp,
|
||||
);
|
||||
}
|
||||
console.log(`## 开始执行... ${globalState.beginTime}\n`);
|
||||
};
|
||||
|
||||
export const runTaskBefore = async () => {
|
||||
if (globalState.isMacos === 0) {
|
||||
await checkServer();
|
||||
}
|
||||
await $`. ${fileTaskBefore} "$@"`;
|
||||
|
||||
if (globalState.taskBefore) {
|
||||
console.log('执行前置命令');
|
||||
await $`eval ${globalState.taskBefore}`;
|
||||
console.log('执行前置命令结束');
|
||||
}
|
||||
};
|
||||
|
||||
export const runTaskAfter = async () => {
|
||||
await $`. ${fileTaskAfter} "$@"`;
|
||||
|
||||
if (globalState.taskAfter) {
|
||||
console.log('执行后置命令');
|
||||
await $`eval "${globalState.taskAfter}"`;
|
||||
console.log('执行后置命令结束');
|
||||
}
|
||||
};
|
||||
|
||||
export const handleTaskEnd = async () => {
|
||||
const etime = new Date();
|
||||
const endTime = formatDate(etime);
|
||||
const endTimestamp = formatTimestamp(etime);
|
||||
let diffTime = endTimestamp - globalState.beginTimestamp;
|
||||
|
||||
if (diffTime === 0) {
|
||||
diffTime = 1;
|
||||
}
|
||||
|
||||
if (globalState.ID) {
|
||||
await updateCron(
|
||||
[globalState.ID],
|
||||
'1',
|
||||
`${process.pid}`,
|
||||
globalState.logPath,
|
||||
globalState.beginTimestamp,
|
||||
diffTime,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`\n## 执行结束... ${endTime} 耗时 ${diffTime} 秒`);
|
||||
};
|
||||
|
||||
initEnv();
|
||||
detectTermux();
|
||||
detectMacos();
|
||||
await importConfig();
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env zx
|
||||
import 'zx/globals';
|
||||
|
||||
import { PassThrough } from 'node:stream';
|
||||
import {
|
||||
listCrontabUser,
|
||||
formatLogTime,
|
||||
formatTimestamp,
|
||||
formatDate,
|
||||
dirLog,
|
||||
dirCli,
|
||||
globalState,
|
||||
handleTaskEnd,
|
||||
} from './share.mjs';
|
||||
import './api.mjs';
|
||||
|
||||
// $.verbose = true;
|
||||
|
||||
async function singleHandle() {}
|
||||
|
||||
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP'];
|
||||
signals.forEach((sig) => process.on(sig, singleHandle));
|
||||
|
||||
export async function handleLogPath(fileParam) {
|
||||
let ID = $.env.ID;
|
||||
if (!ID) {
|
||||
const grepResult =
|
||||
await $`grep -E "task.* ${fileParam}" ${listCrontabUser}`.nothrow();
|
||||
ID = grepResult.stdout.match(/ID=(\d+)/)?.[1];
|
||||
}
|
||||
globalState.ID = ID;
|
||||
|
||||
const suffix = ID && parseInt(ID, 10) > 0 ? `_${ID}` : '';
|
||||
globalState.time = new Date();
|
||||
const logTime = formatLogTime(globalState.time);
|
||||
let logDirTmp = path.basename(fileParam);
|
||||
let logDirTmpPath = '';
|
||||
|
||||
if (fileParam.includes('/')) {
|
||||
logDirTmpPath = path.isAbsolute(fileParam)
|
||||
? fileParam.substring(1)
|
||||
: fileParam;
|
||||
logDirTmpPath = path.basename(path.dirname(logDirTmpPath));
|
||||
}
|
||||
|
||||
if (logDirTmpPath) {
|
||||
logDirTmp = `${logDirTmpPath}_${logDirTmp}`;
|
||||
}
|
||||
|
||||
const logDir = `${logDirTmp.replace(/\.[^/.]+$/, '')}${suffix}`;
|
||||
globalState.logDir = logDir;
|
||||
globalState.logPath = `${logDir}/${logTime}.log`;
|
||||
if ($.env.real_log_path) {
|
||||
globalState.logPath = realLogPath;
|
||||
}
|
||||
|
||||
await $`mkdir -p ${dirLog}/${logDir}`;
|
||||
}
|
||||
|
||||
export function initBeginTime() {
|
||||
globalState.beginTime = formatDate(globalState.time);
|
||||
globalState.beginTimestamp = formatTimestamp(globalState.time);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const taskArgv = minimist(process.argv.slice(3), {
|
||||
'--': true,
|
||||
});
|
||||
const {
|
||||
_: [scriptFile],
|
||||
} = taskArgv;
|
||||
|
||||
await handleLogPath(scriptFile);
|
||||
initBeginTime();
|
||||
|
||||
cd(`${dirCli}`);
|
||||
const logStream = fs.createWriteStream(`${dirLog}/${globalState.logPath}`);
|
||||
const passThrough = new PassThrough();
|
||||
passThrough.pipe(logStream);
|
||||
const p = $`./otask.mjs ${process.argv.slice(
|
||||
3,
|
||||
)} --GlobalState=${JSON.stringify(globalState)} 2>&1`.nothrow();
|
||||
p.stdout.pipe(passThrough).pipe(process.stdout);
|
||||
await p;
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
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 tsx \
|
||||
&& 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:$PNPM_HOME \
|
||||
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
|
||||
|
||||
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"]
|
||||
+17
-41
@@ -1,40 +1,27 @@
|
||||
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 tsx \
|
||||
&& cd /tmp/build \
|
||||
&& pnpm install --prod
|
||||
|
||||
FROM python:3.11-alpine
|
||||
FROM node:alpine
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||
ARG QL_BRANCH=develop
|
||||
ARG QL_BRANCH=master
|
||||
|
||||
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 \
|
||||
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 \
|
||||
LANG=zh_CN.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/
|
||||
WORKDIR ${QL_DIR}
|
||||
|
||||
RUN set -x \
|
||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update -f \
|
||||
&& apk upgrade \
|
||||
&& apk --no-cache add -f bash \
|
||||
coreutils \
|
||||
moreutils \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
@@ -42,41 +29,30 @@ RUN set -x \
|
||||
perl \
|
||||
openssl \
|
||||
nginx \
|
||||
nodejs \
|
||||
python3 \
|
||||
jq \
|
||||
openssh \
|
||||
procps \
|
||||
netcat-openbsd \
|
||||
unzip \
|
||||
npm \
|
||||
py3-pip \
|
||||
&& 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
|
||||
|
||||
ARG SOURCE_COMMIT
|
||||
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
&& 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 \
|
||||
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||
&& 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
|
||||
|
||||
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"]
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
version: '2'
|
||||
services:
|
||||
web:
|
||||
# alpine 基础镜像版本
|
||||
image: whyour/qinglong:latest
|
||||
# debian-slim 基础镜像版本
|
||||
# image: whyour/qinglong:debian
|
||||
volumes:
|
||||
- ./data:/ql/data
|
||||
ports:
|
||||
- "0.0.0.0:5700:5700"
|
||||
environment:
|
||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
QlBaseUrl: '/'
|
||||
restart: unless-stopped
|
||||
|
||||
+25
-12
@@ -1,42 +1,55 @@
|
||||
#!/bin/bash
|
||||
|
||||
dir_shell=/ql/shell
|
||||
. $dir_shell/env.sh
|
||||
. $dir_shell/share.sh
|
||||
link_shell
|
||||
|
||||
echo -e "======================1. 检测配置文件========================\n"
|
||||
make_dir /etc/nginx/conf.d
|
||||
make_dir /run/nginx
|
||||
init_nginx
|
||||
fix_config
|
||||
|
||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||
pm2 l &>/dev/null
|
||||
echo
|
||||
|
||||
echo -e "======================2. 安装依赖========================\n"
|
||||
patch_version
|
||||
update_depend
|
||||
echo
|
||||
|
||||
echo -e "======================3. 启动nginx========================\n"
|
||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||
echo -e "nginx启动成功...\n"
|
||||
|
||||
echo -e "======================4. 启动pm2服务========================\n"
|
||||
reload_update
|
||||
reload_pm2
|
||||
echo -e "======================4. 启动面板监控========================\n"
|
||||
pm2 delete public &>/dev/null
|
||||
pm2 start $dir_static/build/public.js -n public --source-map-support --time
|
||||
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
|
||||
echo -e "======================5. 启动bot========================\n"
|
||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
||||
echo -e "======================7. 启动bot========================\n"
|
||||
nohup ql bot >>$dir_log/start.log 2>&1 &
|
||||
echo -e "bot后台启动中...\n"
|
||||
fi
|
||||
|
||||
if [[ $EnableExtraShell == true ]]; then
|
||||
echo -e "====================6. 执行自定义脚本========================\n"
|
||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
||||
echo -e "======================8. 执行自定义脚本========================\n"
|
||||
nohup ql extra >>$dir_log/start.log 2>&1 &
|
||||
echo -e "自定义脚本后台执行中...\n"
|
||||
fi
|
||||
|
||||
echo -e "############################################################\n"
|
||||
echo -e "容器启动成功..."
|
||||
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
|
||||
echo -e "############################################################\n"
|
||||
|
||||
crond -f >/dev/null
|
||||
|
||||
+12
-45
@@ -1,74 +1,43 @@
|
||||
upstream baseApi {
|
||||
upstream api {
|
||||
server 0.0.0.0:5600;
|
||||
}
|
||||
|
||||
upstream publicApi {
|
||||
upstream public {
|
||||
server 0.0.0.0:5400;
|
||||
}
|
||||
|
||||
upstream updateApi {
|
||||
server 0.0.0.0:5300;
|
||||
}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default keep-alive;
|
||||
'websocket' upgrade;
|
||||
}
|
||||
|
||||
server {
|
||||
IPV4_CONFIG
|
||||
IPV6_CONFIG
|
||||
listen 5700;
|
||||
root /ql/static/dist;
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
location QL_BASE_URLapi/update/ {
|
||||
location /api/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://updateApi/api/;
|
||||
proxy_buffering off;
|
||||
proxy_redirect default;
|
||||
proxy_connect_timeout 1800;
|
||||
proxy_send_timeout 1800;
|
||||
proxy_read_timeout 1800;
|
||||
proxy_pass http://public;
|
||||
}
|
||||
|
||||
location QL_BASE_URLapi/public/ {
|
||||
location /api {
|
||||
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 X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://baseApi/api/;
|
||||
proxy_buffering off;
|
||||
proxy_redirect default;
|
||||
proxy_connect_timeout 1800;
|
||||
proxy_send_timeout 1800;
|
||||
proxy_read_timeout 1800;
|
||||
proxy_pass http://api;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
location QL_BASE_URLopen/ {
|
||||
location /open {
|
||||
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://baseApi/open/;
|
||||
proxy_buffering off;
|
||||
proxy_redirect default;
|
||||
proxy_connect_timeout 1800;
|
||||
proxy_send_timeout 1800;
|
||||
proxy_read_timeout 1800;
|
||||
proxy_pass http://api;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
@@ -79,12 +48,10 @@ server {
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.0;
|
||||
QL_ROOT_CONFIG
|
||||
|
||||
location QL_BASE_URL_LOCATION {
|
||||
QL_ALIAS_CONFIG
|
||||
location / {
|
||||
index index.html index.htm;
|
||||
try_files $uri QL_BASE_URLindex.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~ .*\.(html)$ {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ http {
|
||||
|
||||
server_tokens off;
|
||||
|
||||
client_max_body_size 4096m;
|
||||
client_max_body_size 20m;
|
||||
client_body_buffer_size 20m;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"watch": ["back", ".env"],
|
||||
"ext": "js,ts,json",
|
||||
"exec": "ts-node -P tsconfig.back.json ./back/app.ts"
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "ts-node --transpile-only ./back/app.ts"
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'update',
|
||||
max_restarts: 10,
|
||||
kill_timeout: 15000,
|
||||
wait_ready: true,
|
||||
listen_timeout: 10000,
|
||||
time: true,
|
||||
script: 'static/build/update.js',
|
||||
},
|
||||
],
|
||||
};
|
||||
+34
-86
@@ -2,26 +2,19 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "concurrently -n w: npm:start:*",
|
||||
"start:front": "max dev",
|
||||
"start:front": "umi dev",
|
||||
"start:back": "nodemon",
|
||||
"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",
|
||||
"build:front": "max build",
|
||||
"start:public": "ts-node back/public.ts",
|
||||
"build:front": "umi build",
|
||||
"build:back": "tsc -p tsconfig.back.json",
|
||||
"panel": "npm run build:back && node static/build/app.js",
|
||||
"schedule": "npm run build:back && node static/build/schedule/index.js",
|
||||
"schedule": "npm run build:back && node static/build/schedule.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",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"postinstall": "max setup 2>/dev/null || true",
|
||||
"postinstall": "umi generate tmp 2>/dev/null || true",
|
||||
"test": "umi-test",
|
||||
"test:coverage": "umi-test --coverage"
|
||||
},
|
||||
"bin": {
|
||||
"task": "./cli/task.mjs"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
@@ -35,144 +28,99 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"react",
|
||||
"react-dom",
|
||||
"antd",
|
||||
"dva",
|
||||
"postcss",
|
||||
"webpack",
|
||||
"eslint",
|
||||
"stylelint",
|
||||
"redux",
|
||||
"@babel/core",
|
||||
"monaco-editor",
|
||||
"rc-field-form",
|
||||
"@types/lodash.merge",
|
||||
"rollup",
|
||||
"styled-components"
|
||||
],
|
||||
"allowedVersions": {
|
||||
"react": "18",
|
||||
"react-dom": "18",
|
||||
"dva-core": "2"
|
||||
"react": "17",
|
||||
"react-dom": "17"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.8.13",
|
||||
"@otplib/preset-default": "^12.0.1",
|
||||
"@sentry/node": "^7.12.1",
|
||||
"@sentry/node": "^7.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
"body-parser": "^1.19.2",
|
||||
"celebrate": "^15.0.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^4.2.1",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"dayjs": "^1.11.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"express-jwt": "^6.1.1",
|
||||
"express-rate-limit": "^7.0.0",
|
||||
"express-urlrewrite": "^1.4.0",
|
||||
"form-data": "^4.0.0",
|
||||
"got": "^11.8.2",
|
||||
"hpagent": "^1.2.0",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"hpagent": "^0.1.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"multer": "^1.4.4",
|
||||
"nedb": "^1.8.0",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
"protobufjs": "^7.3.0",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"p-queue": "7.2.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^6.25.5",
|
||||
"sequelize": "^6.20.1",
|
||||
"serve-handler": "^6.1.3",
|
||||
"sockjs": "^0.3.24",
|
||||
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
|
||||
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
|
||||
"toad-scheduler": "^1.6.0",
|
||||
"typedi": "^0.10.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.6.0",
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"zx": "^8.1.4",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"request-ip": "3.3.0",
|
||||
"ip2region": "2.3.0"
|
||||
"yargs": "^17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/pro-layout": "6.38.22",
|
||||
"@monaco-editor/react": "4.2.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@sentry/react": "^7.12.1",
|
||||
"@ant-design/pro-layout": "^6.33.1",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@sentry/react": "^7.0.0",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-jwt": "^6.0.4",
|
||||
"@types/file-saver": "2.0.2",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/lodash": "^4.14.185",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/nedb": "^1.8.12",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/qrcode.react": "^1.0.2",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.13",
|
||||
"@types/serve-handler": "^6.1.1",
|
||||
"@types/sockjs": "^0.3.33",
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/request-ip": "0.0.41",
|
||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||
"@uiw/react-codemirror": "^4.21.9",
|
||||
"@umijs/max": "^4.0.72",
|
||||
"@umijs/plugin-antd": "^0.15.0",
|
||||
"@umijs/plugin-esbuild": "^1.4.1",
|
||||
"@umijs/ssr-darkreader": "^4.9.45",
|
||||
"ahooks": "^3.7.8",
|
||||
"@umijs/test": "^3.5.21",
|
||||
"ansi-to-react": "^6.1.6",
|
||||
"antd": "^4.24.8",
|
||||
"antd": "^4.20.5",
|
||||
"antd-img-crop": "^4.2.3",
|
||||
"axios": "^1.4.0",
|
||||
"codemirror": "^5.65.2",
|
||||
"compression-webpack-plugin": "9.2.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"file-saver": "2.0.2",
|
||||
"lint-staged": "^13.0.3",
|
||||
"monaco-editor": "0.33.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"lint-staged": "^12.3.4",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettier": "^2.5.1",
|
||||
"pretty-bytes": "6.1.1",
|
||||
"qiniu": "^7.4.0",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"query-string": "^7.1.1",
|
||||
"rc-tween-one": "^3.0.6",
|
||||
"rc-virtual-list": "3.5.3",
|
||||
"react": "18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react": "17.x",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-diff-viewer": "^3.1.1",
|
||||
"react-dnd": "^14.0.2",
|
||||
"react-dnd-html5-backend": "^14.0.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-intl-universal": "^2.6.21",
|
||||
"react-dom": "17.x",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.6.0",
|
||||
"ts-node": "^10.6.0",
|
||||
"ts-proto": "^1.146.0",
|
||||
"tslib": "^2.4.0",
|
||||
"tsx": "^4.7.3",
|
||||
"typescript": "5.2.2",
|
||||
"typescript": "^4.6.2",
|
||||
"umi": "^3.5.21",
|
||||
"umi-request": "^1.4.0",
|
||||
"vh-check": "^2.0.5",
|
||||
"virtualizedtableforantd4": "1.3.0",
|
||||
"webpack": "^5.70.0",
|
||||
"yorkie": "^2.0.0"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user