mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 03:10:48 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06723407d2 | ||
|
|
db8dfb32cf | ||
|
|
a7117e4442 | ||
|
|
53414e5d70 | ||
|
|
7bbc776759 | ||
|
|
f123c0bfe1 | ||
|
|
830865ff77 | ||
|
|
15fc6f975c | ||
|
|
4582711867 | ||
|
|
1eb64720d1 | ||
|
|
29f5134fb4 | ||
|
|
55ffd85d48 | ||
|
|
075165f89d | ||
|
|
42dabbf4c0 | ||
|
|
999b5d325f | ||
|
|
027c3f584c | ||
|
|
2d3ebe6087 | ||
|
|
f016e6734a | ||
|
|
9f0503f138 | ||
|
|
f74821895e | ||
|
|
872519a213 | ||
|
|
dcd39ea6d8 | ||
|
|
a4a76ec235 | ||
|
|
87a265191c | ||
|
|
6f4736bddf | ||
|
|
e85742a293 | ||
|
|
28ea160004 | ||
|
|
99d881fc0d | ||
|
|
5d6ac7f07c | ||
|
|
a23dbfe5e1 | ||
|
|
2d818a19d1 | ||
|
|
be89fd4ff7 | ||
|
|
6d2edaaaf8 | ||
|
|
19afbd06c0 | ||
|
|
718964ab86 | ||
|
|
0dadd4e6cc | ||
|
|
57e7d756cb | ||
|
|
9f5fb30334 | ||
|
|
ec8c339f6e | ||
|
|
728d84cb15 | ||
|
|
a7b5a0f4c5 | ||
|
|
b009d3e3fd | ||
|
|
559defc8d6 | ||
|
|
98cc983473 | ||
|
|
6d42a680b1 | ||
|
|
1602f829ba | ||
|
|
c4a4764762 | ||
|
|
ac32117e78 | ||
|
|
3b5bcf8f16 | ||
|
|
8735321e34 | ||
|
|
be76ff4675 | ||
|
|
15678b029c | ||
|
|
05d47be1c8 | ||
|
|
106b34e33a | ||
|
|
488fbd2bbb | ||
|
|
b82c630e3f | ||
|
|
5a01c41bbb | ||
|
|
471e778a61 | ||
|
|
68e576a5e3 | ||
|
|
8c74fca362 | ||
|
|
026fc43695 | ||
|
|
4f925b92d0 | ||
|
|
4200d5e530 | ||
|
|
32ca72133a | ||
|
|
7708638267 | ||
|
|
cb24d3c9fa | ||
|
|
07c63e285e | ||
|
|
a5e8d5cb80 | ||
|
|
8892e8833e | ||
|
|
25af639eef | ||
|
|
58383e9022 | ||
|
|
d244650432 | ||
|
|
f338537ea0 | ||
|
|
8d2c62d6d1 | ||
|
|
8b1906c786 | ||
|
|
3f29c1aa4e | ||
|
|
31ff0476a6 | ||
|
|
b3ed824b2e | ||
|
|
ec97242415 | ||
|
|
d54faf8044 | ||
|
|
5f380b55d1 | ||
|
|
8d46115823 | ||
|
|
cc24e51cd7 | ||
|
|
7a5e6dba51 | ||
|
|
ad1ee0d2ef | ||
|
|
e4f7461b62 | ||
|
|
a9e0a934a7 | ||
|
|
2a23b838f6 | ||
|
|
1afc791e0b | ||
|
|
05cf51afa0 | ||
|
|
76b6677f5a | ||
|
|
af74afd10d | ||
|
|
46512142fa | ||
|
|
6f13cc6e6f | ||
|
|
9dab6660d0 | ||
|
|
89ddfd20cd | ||
|
|
c9bd27ec4f | ||
|
|
2e937ad5ae | ||
|
|
39d3d53614 | ||
|
|
456b4c4136 | ||
|
|
a90e536403 | ||
|
|
d96206ef66 | ||
|
|
ef71e13139 | ||
|
|
189826c5db | ||
|
|
bdecdce118 | ||
|
|
460a1750f4 | ||
|
|
d2c2ea8d3b | ||
|
|
cb12d8ffec | ||
|
|
5bb1afe5c6 | ||
|
|
7caabe9063 | ||
|
|
1695b8c0fe | ||
|
|
0927682abf | ||
|
|
cde4646fe1 | ||
|
|
8c410377b9 | ||
|
|
312aaf18de | ||
|
|
ac2979381f | ||
|
|
03c5c7bbea | ||
|
|
513de1a792 | ||
|
|
c6a8b93b70 | ||
|
|
4c8e097448 | ||
|
|
4e7a5c637d | ||
|
|
f1a51638a5 | ||
|
|
9dcc547ac7 | ||
|
|
bb47d67d0b | ||
|
|
f6a122e5ea | ||
|
|
5523d537dc | ||
|
|
5a21247cbb | ||
|
|
419c5a7c5b | ||
|
|
468ff9c1af | ||
|
|
ebb6290468 | ||
|
|
85d8565241 | ||
|
|
bd35725b3c | ||
|
|
fb6a80e306 | ||
|
|
2c9b283b75 | ||
|
|
0218405998 | ||
|
|
aa59e9a44b | ||
|
|
f9e2e22b85 | ||
|
|
d27ca56a2d | ||
|
|
bf40447ba4 | ||
|
|
89f7b99a94 | ||
|
|
c71d34cbac | ||
|
|
b97ad5cb6b | ||
|
|
081793e1fb | ||
|
|
f76b7d3b35 | ||
|
|
95448418c0 | ||
|
|
199070f829 | ||
|
|
1fa296a53e | ||
|
|
caa8bd7e7f | ||
|
|
22d097aa08 | ||
|
|
5dbfc65e7f | ||
|
|
03e6f18e54 | ||
|
|
84357e9dcc | ||
|
|
8bb8a29400 | ||
|
|
1b893f35ba | ||
|
|
5be421b4ea | ||
|
|
8ad325686d | ||
|
|
54e58c1e08 | ||
|
|
8818a779b5 | ||
|
|
cc65b6c8bb | ||
|
|
523f583516 | ||
|
|
89539deff1 | ||
|
|
6907d520ed | ||
|
|
eae2d6d17a | ||
|
|
82dd9114fd | ||
|
|
58e047fe0e | ||
|
|
d703769d43 | ||
|
|
be6ca54d9c | ||
|
|
a5cadfbcab | ||
|
|
5e3be8fcd1 | ||
|
|
8b5347dd44 | ||
|
|
24f6070f9e | ||
|
|
d00f2cd33b | ||
|
|
8276a19012 | ||
|
|
c769c598b8 | ||
|
|
d687e79b6c | ||
|
|
8fac58d73f | ||
|
|
b2ba86b212 | ||
|
|
92673b4a17 | ||
|
|
85facc039a | ||
|
|
24f33431de | ||
|
|
36533ab7e2 | ||
|
|
97d170c8b1 | ||
|
|
1413999ac3 | ||
|
|
0d23f8a962 | ||
|
|
d0b13a04cc | ||
|
|
fca7b02656 | ||
|
|
2afbfa08c6 | ||
|
|
5fe63666b0 | ||
|
|
9ee08acf29 | ||
|
|
bc7f406968 | ||
|
|
00ee90792f | ||
|
|
4a8c90365e | ||
|
|
d0e46224dc | ||
|
|
54dd90e7c5 | ||
|
|
7ef3de1367 | ||
|
|
ce6aa23d5b | ||
|
|
dc819bcf8e | ||
|
|
b0595656bb | ||
|
|
1dac54b7e7 | ||
|
|
95ea085e1e | ||
|
|
ec5f6d20cb | ||
|
|
aa288c89b3 | ||
|
|
ea419a98ae | ||
|
|
0f0c79f52f | ||
|
|
38df66a5b1 | ||
|
|
0051260171 | ||
|
|
f6dc0e020f | ||
|
|
6f2e7b9459 | ||
|
|
384e3f09da | ||
|
|
747f4552e0 | ||
|
|
1db13653bf | ||
|
|
d05304f839 | ||
|
|
0b551ddc0d | ||
|
|
f117f6b39c | ||
|
|
0c54ca95d9 | ||
|
|
bcbc8a633f | ||
|
|
73963c89fc | ||
|
|
793d14f04b | ||
|
|
ef32181f36 | ||
|
|
419f59818b | ||
|
|
2bda3a276a | ||
|
|
5e5b28ef29 | ||
|
|
8dfbe93135 | ||
|
|
810c6ca76c | ||
|
|
912b4ddb76 | ||
|
|
6b41df3497 | ||
|
|
3e62fedc73 | ||
|
|
a10ffef38f | ||
|
|
816ce31279 | ||
|
|
4e58a640e4 | ||
|
|
43925d6630 | ||
|
|
b688461137 | ||
|
|
1e85528889 | ||
|
|
eb4976ed6b | ||
|
|
7dd87220ab | ||
|
|
9927461ea9 | ||
|
|
84c7ffc509 | ||
|
|
ee52f09bcb | ||
|
|
531fc48bdf | ||
|
|
0581a7ab10 | ||
|
|
f0d2d57584 | ||
|
|
cb154437ac | ||
|
|
7ea9a8aa57 | ||
|
|
7e34f4990d | ||
|
|
f0f2c6b537 | ||
|
|
f71b3d0378 | ||
|
|
7d6e1d3e3d | ||
|
|
cf5f1b6f25 | ||
|
|
903007e6ca | ||
|
|
d6614acf66 | ||
|
|
86df495b67 | ||
|
|
71b0c10997 | ||
|
|
e273c59d78 | ||
|
|
baea40f157 | ||
|
|
a809b89bb0 | ||
|
|
89e08953cb | ||
|
|
e154cf65b6 | ||
|
|
d7162a24be | ||
|
|
16532720d7 | ||
|
|
012ee2419d | ||
|
|
7e4fe2f1b3 | ||
|
|
5a95b72607 | ||
|
|
4d27055605 | ||
|
|
e2b9578253 | ||
|
|
57fd8f378c | ||
|
|
a726f224f8 | ||
|
|
d502ac6e0f | ||
|
|
f6a828f0af | ||
|
|
0a53a7001e | ||
|
|
0ab5494744 | ||
|
|
e60cf15f39 | ||
|
|
b53db97b98 | ||
|
|
f12c75f91b | ||
|
|
3df1a14f72 | ||
|
|
712f105d34 | ||
|
|
4ea757fece | ||
|
|
d8b32f63ea | ||
|
|
771b81afaf | ||
|
|
a4f4c6ac15 | ||
|
|
ae22cf064e | ||
|
|
fbb8e1bd31 | ||
|
|
47202ef66c | ||
|
|
f46c70dcbd | ||
|
|
c7a837e33d | ||
|
|
544e2e8331 | ||
|
|
a62ccbc0db | ||
|
|
891619ad55 | ||
|
|
a3a62f65dd | ||
|
|
3131f197b8 | ||
|
|
e13d9ed51b | ||
|
|
1b5f4a0f9d | ||
|
|
c9c4043a4e | ||
|
|
bb59dd977a | ||
|
|
c2b213a37c | ||
|
|
559f4418d1 | ||
|
|
2149bca7b8 | ||
|
|
be5db82b79 | ||
|
|
01163e3ea3 | ||
|
|
7f986fdb2d | ||
|
|
38e1ba67af | ||
|
|
d3d2e0887e | ||
|
|
a2650715b3 | ||
|
|
8c0685202e | ||
|
|
5b6843b5c0 | ||
|
|
ba341b69ac | ||
|
|
ca89f03724 | ||
|
|
5461325e69 | ||
|
|
e96a2818b7 | ||
|
|
355f06a3cf | ||
|
|
f6e6cfa71c | ||
|
|
9cdfa2ef79 | ||
|
|
6fab22099d | ||
|
|
925819b3aa | ||
|
|
6f96900ab9 | ||
|
|
43bc29b25c | ||
|
|
e13ff86ff6 | ||
|
|
3f3d572932 | ||
|
|
a0ce1562ac | ||
|
|
ee84cbca51 | ||
|
|
3ed5cb48b5 | ||
|
|
499ba5fb99 | ||
|
|
c2f036c370 | ||
|
|
326580b3c8 | ||
|
|
bf59351cf9 | ||
|
|
a954fc3942 | ||
|
|
160a66af23 | ||
|
|
4edbd29b19 | ||
|
|
b6b97478d1 | ||
|
|
27a871e537 | ||
|
|
738b0f9a4a | ||
|
|
1a56dd8ac6 | ||
|
|
672d440725 | ||
|
|
3625b7c0de | ||
|
|
e75a683173 | ||
|
|
89ed8527d6 | ||
|
|
5a5f4b8065 | ||
|
|
d2e23341eb | ||
|
|
233081149b | ||
|
|
5d19ee0ab5 | ||
|
|
653b1cef20 | ||
|
|
f3ecf6e7a0 | ||
|
|
35d8f0a7e8 | ||
|
|
e863dd322a | ||
|
|
fa6dec969f | ||
|
|
58cd05fd09 | ||
|
|
2e6ca5419d | ||
|
|
a29392bb08 | ||
|
|
4dc9c9aa75 | ||
|
|
03c9f79549 | ||
|
|
46aaeb4eac | ||
|
|
4d20bb0de6 | ||
|
|
e001686627 | ||
|
|
931f02ef8e | ||
|
|
a9be3e6376 | ||
|
|
aedd369fc4 | ||
|
|
a1016b7f22 | ||
|
|
603432aa87 | ||
|
|
cd23967ae0 | ||
|
|
dfb159c4ad | ||
|
|
0318f03283 | ||
|
|
9ddf6986e4 | ||
|
|
a28ed9281e | ||
|
|
9cc776aded | ||
|
|
349f14a7fc | ||
|
|
fae26efb88 | ||
|
|
64d0bb18b1 | ||
|
|
864ce071f6 | ||
|
|
5a55934029 | ||
|
|
1f061ad059 | ||
|
|
32f0774df2 | ||
|
|
3206ff6a1e | ||
|
|
2f5e946979 | ||
|
|
5a6d3739d4 | ||
|
|
fa018b84a7 | ||
|
|
a686206e21 | ||
|
|
6132d13261 | ||
|
|
f64c78919d | ||
|
|
1a1067966c | ||
|
|
ec545cabcd | ||
|
|
40b0e90c0d | ||
|
|
5e94be21aa | ||
|
|
417ae8bbde | ||
|
|
acde7ebef6 | ||
|
|
a424e8df7c | ||
|
|
2eed5cba14 | ||
|
|
19e7c238f6 | ||
|
|
f3c939e442 | ||
|
|
3bcf3431d3 | ||
|
|
20d6308bc7 | ||
|
|
96b0127811 | ||
|
|
68cc792d6c | ||
|
|
d0f7449b57 | ||
|
|
b41c1f1d2e | ||
|
|
9a8b72fe3f | ||
|
|
b16f8b96dc | ||
|
|
128429a6a9 | ||
|
|
b849d8b526 | ||
|
|
47b06c3487 | ||
|
|
40e0d143f4 | ||
|
|
c1269d9dde | ||
|
|
6a81ba9069 | ||
|
|
61883776f2 | ||
|
|
f7752cec3a | ||
|
|
6f96c84a16 | ||
|
|
a2ce3304de | ||
|
|
ee0b47d101 | ||
|
|
3b3dd4c26d | ||
|
|
bee1f9968e | ||
|
|
65691cf7c9 | ||
|
|
7b7bcf600d | ||
|
|
18cd7bfa6f | ||
|
|
8c14a31967 | ||
|
|
6c840b435e | ||
|
|
eca615c066 | ||
|
|
22bb785139 | ||
|
|
62fca3dfe2 | ||
|
|
9936097086 | ||
|
|
50dd235e39 | ||
|
|
606bac2e43 | ||
|
|
90ede97797 | ||
|
|
1545cde58a | ||
|
|
8481d81257 | ||
|
|
0688d4f318 | ||
|
|
ab5ab951d7 | ||
|
|
0ca640acbd | ||
|
|
235f99c0ca | ||
|
|
31fdf7f12c | ||
|
|
bd6d711212 | ||
|
|
62596f27ef | ||
|
|
1b88853a8d | ||
|
|
3c5925ff67 | ||
|
|
7d5687d504 | ||
|
|
6b74cccc34 | ||
|
|
1becb26aaf | ||
|
|
bd2917822e | ||
|
|
9c82db4471 | ||
|
|
e4f11156f6 | ||
|
|
c7de19d3b5 | ||
|
|
65ade4b996 | ||
|
|
2b70851074 | ||
|
|
b7b983229b | ||
|
|
ac548433c0 | ||
|
|
02a1afb477 | ||
|
|
e959466898 | ||
|
|
5c5aef64b0 | ||
|
|
86e8b1383c | ||
|
|
9f7bf1f6b8 | ||
|
|
2ff3869608 | ||
|
|
8d21401f88 | ||
|
|
b92daedea2 | ||
|
|
021d89379a | ||
|
|
2b61204233 | ||
|
|
1da1659b51 | ||
|
|
1302a6daa0 | ||
|
|
160dbe780c | ||
|
|
d047cf0d77 | ||
|
|
0c2a2af308 | ||
|
|
bd8953ea81 | ||
|
|
a479f0fbce | ||
|
|
2e00e80e96 | ||
|
|
0bf4aec74e | ||
|
|
bcac3deb6e | ||
|
|
596f93623e | ||
|
|
c74a1bc20c | ||
|
|
795d1b938d | ||
|
|
bad247c51c | ||
|
|
950aed3522 | ||
|
|
8a7bd53f24 | ||
|
|
04230cbd1c | ||
|
|
d0a7e67138 | ||
|
|
56517fd345 | ||
|
|
6315425a7b | ||
|
|
c42299766c | ||
|
|
ef9fa02145 | ||
|
|
51869b6a84 | ||
|
|
d12953a345 | ||
|
|
9f727cd54b | ||
|
|
3de567191e | ||
|
|
b212762417 | ||
|
|
d5f619ff74 | ||
|
|
87027f8a8c | ||
|
|
6fa8a678bf | ||
|
|
f2aa355389 | ||
|
|
b3ef432c6f | ||
|
|
860a1e4dc6 | ||
|
|
f5b322649d | ||
|
|
746650b49a | ||
|
|
43ddc54a70 | ||
|
|
bdfa08bb78 | ||
|
|
07e58a484c | ||
|
|
a652e1e190 | ||
|
|
ec0deeff06 | ||
|
|
7335e2c299 | ||
|
|
003cdd1e06 | ||
|
|
efa1fcfbdf | ||
|
|
7acda0c039 | ||
|
|
cb86fda169 | ||
|
|
324d106693 | ||
|
|
0644edb62e | ||
|
|
0f31cb5fcc | ||
|
|
c80a9d1e65 | ||
|
|
b26d0b4887 | ||
|
|
ed93b919ba | ||
|
|
b1077443a3 | ||
|
|
9455ca64a2 | ||
|
|
d6ebd2699e | ||
|
|
632514d36e | ||
|
|
686baa56f4 | ||
|
|
a3006af7b8 | ||
|
|
ec529bfca6 | ||
|
|
a661b6bd38 | ||
|
|
73e85c2def | ||
|
|
6819487a43 | ||
|
|
14b20873c7 | ||
|
|
bd1ca7e975 | ||
|
|
c01ec828e2 | ||
|
|
47abdb0f36 | ||
|
|
6d4568cdcd | ||
|
|
2068ef7053 | ||
|
|
5655591215 | ||
|
|
feb22f25b8 | ||
|
|
c054259806 | ||
|
|
b179649078 | ||
|
|
a75561399d | ||
|
|
fa02df0d8f | ||
|
|
f17642b243 | ||
|
|
43773222f8 | ||
|
|
91a5e13cf2 | ||
|
|
be4efbad5d | ||
|
|
da3e63e6c3 | ||
|
|
2d427c0670 | ||
|
|
97184d22c3 | ||
|
|
f1cc7641fb | ||
|
|
ca9d742359 | ||
|
|
1d82310ca1 | ||
|
|
687afce245 | ||
|
|
26d986d4a8 | ||
|
|
120599c778 | ||
|
|
54945f2183 | ||
|
|
f48b91dc0a | ||
|
|
4c1551c309 | ||
|
|
558478be16 | ||
|
|
4e403d3d8a | ||
|
|
a79fd74130 | ||
|
|
af2f3deeab | ||
|
|
c15ff89746 | ||
|
|
5b585e0cda | ||
|
|
6619374571 | ||
|
|
9fc8fdd9d1 | ||
|
|
900d279bf6 | ||
|
|
265760146f | ||
|
|
74fa8ffcd1 | ||
|
|
e5313e63a9 | ||
|
|
f482796cdf | ||
|
|
aa08f489d7 | ||
|
|
1cc54b5da6 | ||
|
|
0baf0d23ae | ||
|
|
646443deb5 | ||
|
|
39fb23c025 | ||
|
|
698c463c3a | ||
|
|
6c87634bb3 | ||
|
|
0a9e9e972d | ||
|
|
bb0ad6c325 | ||
|
|
f2dac51a62 | ||
|
|
9534cda1f9 | ||
|
|
27226e7222 | ||
|
|
c51621ee04 | ||
|
|
c78e4447fa | ||
|
|
2d7e525024 | ||
|
|
ecee9e2acd | ||
|
|
d607619f8f | ||
|
|
e8e275cd79 | ||
|
|
4147025406 | ||
|
|
35539a4ccc | ||
|
|
b7a359714f | ||
|
|
bf43bfbd92 | ||
|
|
1265e465d0 | ||
|
|
8f195b964f | ||
|
|
a285efdd85 | ||
|
|
fafb68dbf0 | ||
|
|
3dc87329f1 | ||
|
|
7fc15518b3 | ||
|
|
eb196bd40d | ||
|
|
0cbfca979e | ||
|
|
b3ac81b7b7 | ||
|
|
bb40111d38 | ||
|
|
29a3bea6e4 | ||
|
|
ab4f167bda | ||
|
|
035c69898f | ||
|
|
00cb915f48 | ||
|
|
97baff8baf | ||
|
|
e4da2edf56 | ||
|
|
c1e08ba9f0 | ||
|
|
f00566624d | ||
|
|
2bb55294fc | ||
|
|
86c3e9a843 | ||
|
|
3a998a37f0 | ||
|
|
7eed47cd8e | ||
|
|
8cef0b6216 | ||
|
|
930541d5d6 | ||
|
|
f3518d0cad | ||
|
|
45e2c43997 | ||
|
|
6c0f932ae8 | ||
|
|
5832bf9140 | ||
|
|
47bba19bec | ||
|
|
1e58254f4c | ||
|
|
7739cef7b8 | ||
|
|
9b73392c5e | ||
|
|
79e5b2fb84 | ||
|
|
8ce1a33bd3 | ||
|
|
8ae61f1b23 | ||
|
|
a973c93c04 | ||
|
|
9c0f14aa89 | ||
|
|
f78d72db4c | ||
|
|
fb0c1c6993 | ||
|
|
4d99695e4c | ||
|
|
18321943d6 | ||
|
|
67819d2817 | ||
|
|
ad74e014ef | ||
|
|
bf661dffcf | ||
|
|
11484175c8 | ||
|
|
7bc5004683 | ||
|
|
1d01508164 | ||
|
|
4cae9bcfb9 | ||
|
|
9c84c7bd38 | ||
|
|
5a5e81eae0 | ||
|
|
b0f6bb3ea6 | ||
|
|
0c004606b5 | ||
|
|
fa4978389c | ||
|
|
8b8359b08f | ||
|
|
1d4fec3979 | ||
|
|
bb4123805c | ||
|
|
f49f605c11 | ||
|
|
5496689d7c | ||
|
|
bad8bbdb7c | ||
|
|
76184227a4 | ||
|
|
bbc9ee120d | ||
|
|
85093b2856 | ||
|
|
8c355b6159 | ||
|
|
622037ee98 | ||
|
|
54bb13aad0 | ||
|
|
61e41269ba | ||
|
|
f7cffe3d2d | ||
|
|
0cd39f0c97 | ||
|
|
c09b22358b | ||
|
|
18215b8336 | ||
|
|
f0fae716cc | ||
|
|
7092be97eb | ||
|
|
d9a9d2ee4f | ||
|
|
f86892cef0 | ||
|
|
a48b915675 | ||
|
|
5e13264393 | ||
|
|
ff481ef7f3 | ||
|
|
82a4485792 | ||
|
|
538876f529 | ||
|
|
609a31455b | ||
|
|
2d6da9ebfe | ||
|
|
594e871744 | ||
|
|
95074eb028 |
+5
-3
@@ -1,6 +1,4 @@
|
|||||||
MONGODB_URI='mongodb://'
|
PUBLIC_PORT=5400
|
||||||
YIYAN_MONGODB_URI=''
|
|
||||||
|
|
||||||
CRON_PORT=5500
|
CRON_PORT=5500
|
||||||
BACK_PORT=5600
|
BACK_PORT=5600
|
||||||
PORT=5700
|
PORT=5700
|
||||||
@@ -8,3 +6,7 @@ PORT=5700
|
|||||||
LOG_LEVEL='debug'
|
LOG_LEVEL='debug'
|
||||||
|
|
||||||
SECRET='whyour'
|
SECRET='whyour'
|
||||||
|
|
||||||
|
QINIU_AK=''
|
||||||
|
QINIU_SK=''
|
||||||
|
QINIU_SCOPE=''
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
name: Build QL BASE
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '00 18 * * *' # GMT 14:00 => 北京时间 2:00
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
|
|
||||||
- name: Set time zone
|
|
||||||
uses: szenius/set-timezone@v1.0
|
|
||||||
with:
|
|
||||||
timezoneLinux: "Asia/Shanghai"
|
|
||||||
timezoneMacos: "Asia/Shanghai"
|
|
||||||
timezoneWindows: "China Standard Time"
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
build-args: |
|
|
||||||
MAINTAINER=${{ github.repository_owner }}
|
|
||||||
QL_BRANCH=${{ github.ref_name }}
|
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
|
|
||||||
context: .
|
|
||||||
file: docker/base/Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: whyour/ql:base
|
|
||||||
@@ -20,25 +20,24 @@ jobs:
|
|||||||
build-static:
|
build-static:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v3
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
|
|
||||||
- name: build front and back
|
- name: build front and back
|
||||||
run: |
|
run: |
|
||||||
yarn install
|
npm i -g pnpm
|
||||||
yarn build:front
|
pnpm install
|
||||||
yarn build:back
|
pnpm build:front
|
||||||
|
pnpm build:back
|
||||||
|
|
||||||
- name: copy to static repo
|
- name: copy to static repo
|
||||||
env:
|
env:
|
||||||
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
||||||
GITHUB_BRANCH: ${{ github.ref_name }}
|
GITHUB_BRANCH: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p static
|
mkdir -p tmp
|
||||||
cd ./static
|
cd ./tmp
|
||||||
cp -rf ../dist ./ && cp -rf ../build ./
|
cp -rf ../static/* ./
|
||||||
git init -b ${GITHUB_BRANCH} && git add .
|
git init -b ${GITHUB_BRANCH} && git add .
|
||||||
git config --local user.name 'github-actions[bot]'
|
git config --local user.name 'github-actions[bot]'
|
||||||
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
|
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
@@ -55,10 +54,8 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v3
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
|
|
||||||
- name: Set time zone
|
- name: Set time zone
|
||||||
uses: szenius/set-timezone@v1.0
|
uses: szenius/set-timezone@v1.0
|
||||||
@@ -68,13 +65,13 @@ jobs:
|
|||||||
timezoneWindows: "China Standard Time"
|
timezoneWindows: "China Standard Time"
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -82,7 +79,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ github.repository }}
|
${{ github.repository }}
|
||||||
@@ -99,19 +96,20 @@ jobs:
|
|||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
build-args: |
|
build-args: |
|
||||||
MAINTAINER=${{ github.repository_owner }}
|
MAINTAINER=${{ github.repository_owner }}
|
||||||
QL_BRANCH=${{ github.ref_name }}
|
QL_BRANCH=${{ github.ref_name }}
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
|
network: host
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
context: docker/
|
context: docker/
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
name: PublishDockerImage
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout base
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- name: Docker login
|
|
||||||
env:
|
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
|
|
||||||
|
|
||||||
- name: Docker buildx image and push on master branch
|
|
||||||
env:
|
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
run: |
|
|
||||||
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
|
|
||||||
+3
-2
@@ -4,12 +4,13 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/npm-debug.log*
|
/npm-debug.log*
|
||||||
/yarn-error.log
|
/yarn-error.log
|
||||||
|
/yarn.lock
|
||||||
/pnpm-lock.yaml
|
/pnpm-lock.yaml
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/dist
|
/static
|
||||||
/build
|
/data
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
sentrycli_cdnurl=https://cdn.npm.taobao.org/dist/sentry-cli
|
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
|
||||||
|
strict-peer-dependencies=false
|
||||||
@@ -4,15 +4,22 @@ const CompressionPlugin = require('compression-webpack-plugin');
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
hash: true,
|
hash: true,
|
||||||
layout: false,
|
layout: false,
|
||||||
|
outputPath: 'static/dist',
|
||||||
nodeModulesTransform: {
|
nodeModulesTransform: {
|
||||||
type: 'none',
|
type: 'none',
|
||||||
},
|
},
|
||||||
fastRefresh: {},
|
fastRefresh: {},
|
||||||
|
esbuild: {},
|
||||||
|
webpack5: {},
|
||||||
dynamicImport: {
|
dynamicImport: {
|
||||||
loading: '@/components/pageLoading',
|
loading: '@/components/pageLoading',
|
||||||
},
|
},
|
||||||
favicon: '/images/g5.ico',
|
favicon: '/images/g5.ico',
|
||||||
proxy: {
|
proxy: {
|
||||||
|
'/api/public': {
|
||||||
|
target: 'http://127.0.0.1:5400/',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:5600/',
|
target: 'http://127.0.0.1:5600/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
|||||||
@@ -1,22 +1,201 @@
|
|||||||
MIT LICENSE
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
Copyright (c) 2021-present whyour
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
1. Definitions.
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
included in all copies or substantial portions of the Software.
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
the copyright owner that is granting the License.
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
other entities that control, are controlled by, or are under common
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
control with that entity. For the purposes of this definition,
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2021 WHYOUR <https://github.com/whyour>.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
<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">
|
||||||
|
|
||||||
|
Timed task management panel with python3, javaScript, shell, typescript support
|
||||||
|
|
||||||
|
[![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]
|
||||||
|
|
||||||
|
[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
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[](https://whyour.cn)
|
||||||
|
|
||||||
|
[简体中文](./README.md) | English
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Support for multiple scripting languages (python3, javaScript, shell, typescript)
|
||||||
|
- Support online management of scripts, environment variables, configuration files
|
||||||
|
- Support online view task log
|
||||||
|
- Support second-level task setting
|
||||||
|
- Support system level notification
|
||||||
|
- Support dark mode
|
||||||
|
- Support cell phone operation
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Local Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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:/ql/data \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
--restart unless-stopped \
|
||||||
|
whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker-compose Deployment
|
||||||
|
|
||||||
|
1. docker-compose installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
|
```
|
||||||
|
|
||||||
|
2. start the container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir qinglong
|
||||||
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||||
|
|
||||||
|
# start
|
||||||
|
docker-compose up -d
|
||||||
|
# stop
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
1. built-in commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update and restart Green Dragon
|
||||||
|
ql update
|
||||||
|
# Run custom scripts extra.sh
|
||||||
|
ql extra
|
||||||
|
# Adding a single script file
|
||||||
|
ql raw <file_url>
|
||||||
|
# Add a specific script for a single repository
|
||||||
|
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
||||||
|
# Delete old logs
|
||||||
|
ql rmlog <days>
|
||||||
|
# Start bot
|
||||||
|
ql bot
|
||||||
|
# Detecting the Green Dragon environment and repairing it
|
||||||
|
ql check
|
||||||
|
# Reset the number of login errors
|
||||||
|
ql resetlet
|
||||||
|
# Disable two-step login
|
||||||
|
ql resettfa
|
||||||
|
|
||||||
|
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
|
||||||
|
task <file_path>
|
||||||
|
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
|
||||||
|
# and the foreground will output the day, while recorded in the log file
|
||||||
|
task <file_path> now
|
||||||
|
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
|
||||||
|
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
|
||||||
|
task <file_path> conc <env_name> <account_number>(Optional)
|
||||||
|
# Specify the account to execute and run immediately regardless of whether a random delay is set
|
||||||
|
task <file_path> desi <env_name> <account_number>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. parameter description
|
||||||
|
|
||||||
|
* file_url: Script address
|
||||||
|
* repo_url: Repository address
|
||||||
|
* whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
|
||||||
|
* blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
|
||||||
|
* dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
|
||||||
|
* branch: Pull the branch of the repository
|
||||||
|
* days: Number of days of logs to be kept
|
||||||
|
* file_path: File path for task execution
|
||||||
|
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
|
||||||
|
* account_number: Specify the account number of an environment variable to be executed when the task is executed
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [nevinee](https://gitee.com/evine)
|
||||||
|
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||||
|
- [Ant Design](https://ant.design)
|
||||||
|
- [Ant Design Pro](https://pro.ant.design/)
|
||||||
|
- [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".
|
||||||
|
|
||||||
|
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
|
||||||
|
|
||||||
|
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
支持python3、javaScript、shell、typescript 的定时任务管理面板(A timed task management panel that supports typescript, javaScript, python3, and shell.)
|
支持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]
|
[![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]
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[](https://whyour.cn)
|
[](https://whyour.cn)
|
||||||
|
|
||||||
简体中文 | [English](./README-en.md)
|
简体中文 | [English](./README-en.md)
|
||||||
|
|
||||||
@@ -44,6 +44,27 @@
|
|||||||
# 待完善
|
# 待完善
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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 部署
|
### docker 部署
|
||||||
|
|
||||||
1. docker 安装
|
1. docker 安装
|
||||||
@@ -52,16 +73,31 @@
|
|||||||
sudo curl -sSL get.docker.com | sh
|
sudo curl -sSL get.docker.com | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 启动容器
|
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
|
```bash
|
||||||
docker run -dit \
|
docker run -dit \
|
||||||
-v $PWD/ql/config:/ql/config \
|
-v $PWD/ql:/ql/data \
|
||||||
-v $PWD/ql/log:/ql/log \
|
|
||||||
-v $PWD/ql/db:/ql/db \
|
|
||||||
-v $PWD/ql/repo:/ql/repo \
|
|
||||||
-v $PWD/ql/raw:/ql/raw \
|
|
||||||
-v $PWD/ql/scripts:/ql/scripts \
|
|
||||||
-p 5700:5700 \
|
-p 5700:5700 \
|
||||||
--name qinglong \
|
--name qinglong \
|
||||||
--hostname qinglong \
|
--hostname qinglong \
|
||||||
@@ -81,7 +117,7 @@ sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-c
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir qinglong
|
mkdir qinglong
|
||||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker-compose.yml
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||||
|
|
||||||
# 启动
|
# 启动
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
@@ -101,7 +137,7 @@ ql extra
|
|||||||
# 添加单个脚本文件
|
# 添加单个脚本文件
|
||||||
ql raw <file_url>
|
ql raw <file_url>
|
||||||
# 添加单个仓库的指定脚本
|
# 添加单个仓库的指定脚本
|
||||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
||||||
# 删除旧日志
|
# 删除旧日志
|
||||||
ql rmlog <days>
|
ql rmlog <days>
|
||||||
# 启动tg-bot
|
# 启动tg-bot
|
||||||
@@ -127,9 +163,10 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
|
|
||||||
* file_url: 脚本地址
|
* file_url: 脚本地址
|
||||||
* repo_url: 仓库地址
|
* repo_url: 仓库地址
|
||||||
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
|
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
|
||||||
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
|
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
|
||||||
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
|
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
|
||||||
|
* extensions: 拉取仓库的文件后缀,多个竖线分割
|
||||||
* branch: 拉取仓库的分支
|
* branch: 拉取仓库的分支
|
||||||
* days: 需要保留的日志的天数
|
* days: 需要保留的日志的天数
|
||||||
* file_path: 任务执行时的文件路径
|
* file_path: 任务执行时的文件路径
|
||||||
@@ -152,8 +189,10 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
$ git clone git@github.com:whyour/qinglong.git
|
$ git clone git@github.com:whyour/qinglong.git
|
||||||
$ cd qinglong
|
$ cd qinglong
|
||||||
$ cp .env.example .env
|
$ cp .env.example .env
|
||||||
$ yarn install
|
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
||||||
$ yarn start
|
$ npm install -g pnpm
|
||||||
|
$ pnpm install
|
||||||
|
$ pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
打开你的浏览器,访问 http://127.0.0.1:5700
|
打开你的浏览器,访问 http://127.0.0.1:5700
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -51,7 +50,6 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -76,7 +74,6 @@ export default (app: Router) => {
|
|||||||
fs.writeFileSync(path, content);
|
fs.writeFileSync(path, content);
|
||||||
res.send({ code: 200, message: '保存成功' });
|
res.send({ code: 200, message: '保存成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+26
-22
@@ -42,7 +42,6 @@ export default (app: Router) => {
|
|||||||
return res.send({ code: 400, message: 'param schedule error' });
|
return res.send({ code: 400, message: 'param schedule error' });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -60,7 +59,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.run(req.body);
|
const data = await cronService.run(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -78,7 +76,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.stop(req.body);
|
const data = await cronService.stop(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -102,7 +99,6 @@ export default (app: Router) => {
|
|||||||
);
|
);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -123,7 +119,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.addLabels(req.body.ids, req.body.labels);
|
const data = await cronService.addLabels(req.body.ids, req.body.labels);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -141,7 +136,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.disabled(req.body);
|
const data = await cronService.disabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -159,7 +153,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.enabled(req.body);
|
const data = await cronService.enabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -179,7 +172,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.log(req.params.id);
|
const data = await cronService.log(req.params.id);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -190,9 +182,9 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
labels: Joi.array().optional().allow(null),
|
labels: Joi.array().optional().allow(null),
|
||||||
command: Joi.string().optional(),
|
command: Joi.string().required(),
|
||||||
schedule: Joi.string().optional(),
|
schedule: Joi.string().required(),
|
||||||
name: Joi.string().optional(),
|
name: Joi.string().optional().allow(null),
|
||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -210,7 +202,6 @@ export default (app: Router) => {
|
|||||||
return res.send({ code: 400, message: 'param schedule error' });
|
return res.send({ code: 400, message: 'param schedule error' });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -228,7 +219,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.remove(req.body);
|
const data = await cronService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -246,7 +236,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.pin(req.body);
|
const data = await cronService.pin(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -264,7 +253,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.unPin(req.body);
|
const data = await cronService.unPin(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -279,7 +267,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.import_crontab();
|
const data = await cronService.import_crontab();
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -299,7 +286,6 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.getDb({ id: req.params.id });
|
const data = await cronService.getDb({ id: req.params.id });
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -311,10 +297,10 @@ export default (app: Router) => {
|
|||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
ids: Joi.array().items(Joi.number().required()),
|
ids: Joi.array().items(Joi.number().required()),
|
||||||
status: Joi.string().required(),
|
status: Joi.string().required(),
|
||||||
pid: Joi.string().optional(),
|
pid: Joi.string().optional().allow(null),
|
||||||
log_path: Joi.string().optional(),
|
log_path: Joi.string().optional().allow(null),
|
||||||
last_running_time: Joi.number().optional(),
|
last_running_time: Joi.number().optional().allow(null),
|
||||||
last_execution_time: Joi.number().optional(),
|
last_execution_time: Joi.number().optional().allow(null),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -328,7 +314,25 @@ export default (app: Router) => {
|
|||||||
});
|
});
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/:id/logs',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.logs(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export default (app: Router) => {
|
|||||||
const data = await dependenceService.create(req.body);
|
const data = await dependenceService.create(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -61,7 +60,6 @@ export default (app: Router) => {
|
|||||||
const data = await dependenceService.update(req.body);
|
const data = await dependenceService.update(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -79,7 +77,6 @@ export default (app: Router) => {
|
|||||||
const data = await dependenceService.remove(req.body);
|
const data = await dependenceService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -94,10 +91,9 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.removeDb(req.body);
|
const data = await dependenceService.remove(req.body, true);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -117,7 +113,6 @@ export default (app: Router) => {
|
|||||||
const data = await dependenceService.getDb({ id: req.params.id });
|
const data = await dependenceService.getDb({ id: req.params.id });
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -135,7 +130,6 @@ export default (app: Router) => {
|
|||||||
const data = await dependenceService.reInstall(req.body);
|
const data = await dependenceService.reInstall(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-9
@@ -38,7 +38,6 @@ export default (app: Router) => {
|
|||||||
const data = await envService.create(req.body);
|
const data = await envService.create(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -61,7 +60,6 @@ export default (app: Router) => {
|
|||||||
const data = await envService.update(req.body);
|
const data = await envService.update(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -79,7 +77,6 @@ export default (app: Router) => {
|
|||||||
const data = await envService.remove(req.body);
|
const data = await envService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -103,7 +100,6 @@ export default (app: Router) => {
|
|||||||
const data = await envService.move(req.params.id, req.body);
|
const data = await envService.move(req.params.id, req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -121,7 +117,6 @@ export default (app: Router) => {
|
|||||||
const data = await envService.disabled(req.body);
|
const data = await envService.disabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -139,7 +134,6 @@ export default (app: Router) => {
|
|||||||
const data = await envService.enabled(req.body);
|
const data = await envService.enabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -160,7 +154,6 @@ export default (app: Router) => {
|
|||||||
const data = await envService.updateNames(req.body);
|
const data = await envService.updateNames(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -177,10 +170,9 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.getDb(req.params.id);
|
const data = await envService.getDb({ id: req.params.id });
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import script from './script';
|
|||||||
import open from './open';
|
import open from './open';
|
||||||
import dependence from './dependence';
|
import dependence from './dependence';
|
||||||
import system from './system';
|
import system from './system';
|
||||||
|
import subscription from './subscription';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
@@ -20,6 +21,7 @@ export default () => {
|
|||||||
open(app);
|
open(app);
|
||||||
dependence(app);
|
dependence(app);
|
||||||
system(app);
|
system(app);
|
||||||
|
subscription(app);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
+16
-43
@@ -3,8 +3,10 @@ import { Container } from 'typedi';
|
|||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { getFileContentByName } from '../config/util';
|
import { getFileContentByName, readDirs } from '../config/util';
|
||||||
|
import { join } from 'path';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
const blacklist = ['.tmp'];
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/logs', route);
|
app.use('/logs', route);
|
||||||
@@ -12,62 +14,33 @@ export default (app: Router) => {
|
|||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const fileList = fs.readdirSync(config.logPath, 'utf-8');
|
const result = readDirs(config.logPath, config.logPath, blacklist);
|
||||||
const dirs = [];
|
res.send({
|
||||||
for (let i = 0; i < fileList.length; i++) {
|
code: 200,
|
||||||
const stat = fs.lstatSync(config.logPath + fileList[i]);
|
data: result,
|
||||||
if (stat.isDirectory()) {
|
|
||||||
const fileListTmp = fs.readdirSync(
|
|
||||||
`${config.logPath}/${fileList[i]}`,
|
|
||||||
'utf-8',
|
|
||||||
);
|
|
||||||
dirs.push({
|
|
||||||
name: fileList[i],
|
|
||||||
isDir: true,
|
|
||||||
files: fileListTmp.reverse(),
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
dirs.push({
|
|
||||||
name: fileList[i],
|
|
||||||
isDir: false,
|
|
||||||
files: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.send({ code: 200, dirs });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/:dir/:file',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const { dir, file } = req.params;
|
|
||||||
const content = getFileContentByName(
|
|
||||||
`${config.logPath}/${dir}/${file}`,
|
|
||||||
);
|
|
||||||
res.send({ code: 200, data: content });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/:file',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const { file } = req.params;
|
if (blacklist.includes(req.path)) {
|
||||||
const content = getFileContentByName(`${config.logPath}/${file}`);
|
return res.send({ code: 403, message: '暂无权限' });
|
||||||
|
}
|
||||||
|
const filePath = join(
|
||||||
|
config.logPath,
|
||||||
|
(req.query.path || '') as string,
|
||||||
|
req.params.file,
|
||||||
|
);
|
||||||
|
const content = getFileContentByName(filePath);
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-7
@@ -16,7 +16,6 @@ export default (app: Router) => {
|
|||||||
const data = await openService.list();
|
const data = await openService.list();
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -26,7 +25,7 @@ export default (app: Router) => {
|
|||||||
'/apps',
|
'/apps',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
name: Joi.string().optional().allow(''),
|
name: Joi.string().optional().allow('').disallow('system'),
|
||||||
scopes: Joi.array().items(Joi.string().required()),
|
scopes: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -37,7 +36,6 @@ export default (app: Router) => {
|
|||||||
const data = await openService.create(req.body);
|
const data = await openService.create(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -59,7 +57,6 @@ export default (app: Router) => {
|
|||||||
const data = await openService.update(req.body);
|
const data = await openService.update(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -77,7 +74,6 @@ export default (app: Router) => {
|
|||||||
const data = await openService.remove(req.body);
|
const data = await openService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -97,7 +93,6 @@ export default (app: Router) => {
|
|||||||
const data = await openService.resetSecret(req.params.id);
|
const data = await openService.resetSecret(req.params.id);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -118,7 +113,6 @@ export default (app: Router) => {
|
|||||||
const result = await openService.authToken(req.query as any);
|
const result = await openService.authToken(req.query as any);
|
||||||
return res.send(result);
|
return res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+45
-80
@@ -1,7 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
fileExist,
|
fileExist,
|
||||||
getFileContentByName,
|
getFileContentByName,
|
||||||
|
readDirs,
|
||||||
getLastModifyFilePath,
|
getLastModifyFilePath,
|
||||||
|
readDir,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
@@ -9,65 +11,38 @@ import { Logger } from 'winston';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import path, { join } from 'path';
|
import path, { join, parse } from 'path';
|
||||||
import ScriptService from '../services/script';
|
import ScriptService from '../services/script';
|
||||||
|
import multer from 'multer';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.scriptPath);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, file.originalname);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/scripts', route);
|
app.use('/scripts', route);
|
||||||
|
|
||||||
route.get(
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
'/files',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const fileList = fs.readdirSync(config.scriptPath, 'utf-8');
|
|
||||||
|
|
||||||
let result = [];
|
let result = [];
|
||||||
for (let i = 0; i < fileList.length; i++) {
|
const blacklist = ['node_modules', '.git'];
|
||||||
const fileOrDir = fileList[i];
|
if (req.query.path) {
|
||||||
const fPath = path.join(config.scriptPath, fileOrDir);
|
const targetPath = path.join(
|
||||||
const dirStat = fs.statSync(fPath);
|
config.scriptPath,
|
||||||
if (['node_modules'].includes(fileOrDir)) {
|
req.query.path as string,
|
||||||
continue;
|
);
|
||||||
}
|
result = readDir(targetPath, config.scriptPath, blacklist);
|
||||||
|
|
||||||
if (dirStat.isDirectory()) {
|
|
||||||
const childFileList = fs.readdirSync(fPath, 'utf-8');
|
|
||||||
let children = [];
|
|
||||||
for (let j = 0; j < childFileList.length; j++) {
|
|
||||||
const childFile = childFileList[j];
|
|
||||||
const sPath = path.join(config.scriptPath, fileOrDir, childFile);
|
|
||||||
const _fileExist = await fileExist(sPath);
|
|
||||||
if (_fileExist && fs.statSync(sPath).isFile()) {
|
|
||||||
const statObj = fs.statSync(sPath);
|
|
||||||
children.push({
|
|
||||||
title: childFile,
|
|
||||||
value: childFile,
|
|
||||||
key: `${fileOrDir}/${childFile}`,
|
|
||||||
mtime: statObj.mtimeMs,
|
|
||||||
parent: fileOrDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.push({
|
|
||||||
title: fileOrDir,
|
|
||||||
value: fileOrDir,
|
|
||||||
key: fileOrDir,
|
|
||||||
mtime: dirStat.mtimeMs,
|
|
||||||
disabled: true,
|
|
||||||
children: children.sort((a, b) => b.mtime - a.mtime),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
result.push({
|
result = readDirs(config.scriptPath, config.scriptPath, blacklist);
|
||||||
title: fileOrDir,
|
|
||||||
value: fileOrDir,
|
|
||||||
key: fileOrDir,
|
|
||||||
mtime: dirStat.mtimeMs,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
code: 200,
|
code: 200,
|
||||||
data: result,
|
data: result,
|
||||||
@@ -76,8 +51,7 @@ export default (app: Router) => {
|
|||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/:file',
|
||||||
@@ -92,7 +66,6 @@ export default (app: Router) => {
|
|||||||
const content = getFileContentByName(filePath);
|
const content = getFileContentByName(filePath);
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -100,14 +73,7 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/',
|
||||||
celebrate({
|
upload.single('file'),
|
||||||
body: Joi.object({
|
|
||||||
filename: Joi.string().required(),
|
|
||||||
path: Joi.string().optional().allow(''),
|
|
||||||
content: Joi.string().allow(''),
|
|
||||||
originFilename: Joi.string().allow(''),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
@@ -117,6 +83,7 @@ export default (app: Router) => {
|
|||||||
content: string;
|
content: string;
|
||||||
originFilename: string;
|
originFilename: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
path = config.scriptPath;
|
path = config.scriptPath;
|
||||||
}
|
}
|
||||||
@@ -129,18 +96,21 @@ export default (app: Router) => {
|
|||||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||||
return res.send({
|
return res.send({
|
||||||
code: 430,
|
code: 430,
|
||||||
data: '文件路径禁止访问',
|
message: '文件路径禁止访问',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
fs.renameSync(req.file.path, join(path, req.file.filename));
|
||||||
|
return res.send({ code: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
if (!originFilename) {
|
if (!originFilename) {
|
||||||
originFilename = filename;
|
originFilename = filename;
|
||||||
}
|
}
|
||||||
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
|
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
|
||||||
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
||||||
if (fs.existsSync(originFilePath)) {
|
if (fs.existsSync(originFilePath)) {
|
||||||
if (!fs.existsSync(config.bakPath)) {
|
|
||||||
fs.mkdirSync(config.bakPath);
|
|
||||||
}
|
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
originFilePath,
|
originFilePath,
|
||||||
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
|
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
|
||||||
@@ -152,7 +122,6 @@ export default (app: Router) => {
|
|||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -164,7 +133,7 @@ export default (app: Router) => {
|
|||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().optional().allow(''),
|
path: Joi.string().optional().allow(''),
|
||||||
content: Joi.string().required(),
|
content: Joi.string().required().allow(''),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -179,7 +148,6 @@ export default (app: Router) => {
|
|||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -204,7 +172,6 @@ export default (app: Router) => {
|
|||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -235,7 +202,6 @@ export default (app: Router) => {
|
|||||||
return next(err);
|
return next(err);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -246,22 +212,22 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
|
content: Joi.string().optional().allow(''),
|
||||||
path: Joi.string().optional().allow(''),
|
path: Joi.string().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path } = req.body as {
|
let { filename, content, path } = req.body;
|
||||||
filename: string;
|
const { name, ext } = parse(filename);
|
||||||
path: string;
|
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||||
};
|
fs.writeFileSync(filePath, content || '', { encoding: 'utf8' });
|
||||||
const filePath = join(path, filename);
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.runScript(filePath);
|
const result = await scriptService.runScript(filePath);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -272,22 +238,21 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
|
content: Joi.string().optional().allow(''),
|
||||||
path: Joi.string().optional().allow(''),
|
path: Joi.string().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path } = req.body as {
|
let { filename, content, path } = req.body;
|
||||||
filename: string;
|
const { name, ext } = parse(filename);
|
||||||
path: string;
|
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||||
};
|
|
||||||
const filePath = join(path, filename);
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.stopScript(filePath);
|
const result = await scriptService.stopScript(filePath);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import SubscriptionService from '../services/subscription';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import cron_parser from 'cron-parser';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/subscriptions', route);
|
||||||
|
|
||||||
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.list(
|
||||||
|
req.query.searchValue as string,
|
||||||
|
);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
type: Joi.string().required(),
|
||||||
|
schedule: Joi.string().optional().allow('').allow(null),
|
||||||
|
interval_schedule: Joi.object({
|
||||||
|
type: Joi.string().required(),
|
||||||
|
value: Joi.number().min(1).required(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.allow('')
|
||||||
|
.allow(null),
|
||||||
|
name: Joi.string().optional().allow('').allow(null),
|
||||||
|
url: Joi.string().required(),
|
||||||
|
whitelist: Joi.string().optional().allow('').allow(null),
|
||||||
|
blacklist: Joi.string().optional().allow('').allow(null),
|
||||||
|
branch: Joi.string().optional().allow('').allow(null),
|
||||||
|
dependences: Joi.string().optional().allow('').allow(null),
|
||||||
|
pull_type: Joi.string().optional().allow('').allow(null),
|
||||||
|
pull_option: Joi.object().optional().allow('').allow(null),
|
||||||
|
extensions: Joi.string().optional().allow('').allow(null),
|
||||||
|
sub_before: Joi.string().optional().allow('').allow(null),
|
||||||
|
sub_after: Joi.string().optional().allow('').allow(null),
|
||||||
|
schedule_type: Joi.string().required(),
|
||||||
|
alias: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!req.body.schedule ||
|
||||||
|
cron_parser.parseExpression(req.body.schedule).hasNext()
|
||||||
|
) {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.create(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} else {
|
||||||
|
return res.send({ code: 400, message: 'param schedule error' });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/run',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.run(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/stop',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.stop(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/disable',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.disabled(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/enable',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.enabled(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/:id/log',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.log(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
type: Joi.string().required(),
|
||||||
|
schedule: Joi.string().optional().allow('').allow(null),
|
||||||
|
interval_schedule: Joi.object().optional().allow('').allow(null),
|
||||||
|
name: Joi.string().optional().allow('').allow(null),
|
||||||
|
url: Joi.string().required(),
|
||||||
|
whitelist: Joi.string().optional().allow('').allow(null),
|
||||||
|
blacklist: Joi.string().optional().allow('').allow(null),
|
||||||
|
branch: Joi.string().optional().allow('').allow(null),
|
||||||
|
dependences: Joi.string().optional().allow('').allow(null),
|
||||||
|
pull_type: Joi.string().optional().allow('').allow(null),
|
||||||
|
pull_option: Joi.object().optional().allow('').allow(null),
|
||||||
|
schedule_type: Joi.string().optional().allow('').allow(null),
|
||||||
|
extensions: Joi.string().optional().allow('').allow(null),
|
||||||
|
sub_before: Joi.string().optional().allow('').allow(null),
|
||||||
|
sub_after: Joi.string().optional().allow('').allow(null),
|
||||||
|
alias: Joi.string().required(),
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!req.body.schedule ||
|
||||||
|
typeof req.body.schedule === 'object' ||
|
||||||
|
cron_parser.parseExpression(req.body.schedule).hasNext()
|
||||||
|
) {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.update(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} else {
|
||||||
|
return res.send({ code: 400, message: 'param schedule error' });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
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);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/:id',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.getDb({ id: req.params.id });
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/status',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
ids: Joi.array().items(Joi.number().required()),
|
||||||
|
status: Joi.string().required(),
|
||||||
|
pid: Joi.string().optional(),
|
||||||
|
log_path: Joi.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.status({
|
||||||
|
...req.body,
|
||||||
|
status: parseInt(req.body.status),
|
||||||
|
pid: parseInt(req.body.pid) || '',
|
||||||
|
});
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/:id/logs',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const data = await subscriptionService.logs(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
+23
-7
@@ -5,8 +5,8 @@ import * as fs from 'fs';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import { getFileContentByName } from '../config/util';
|
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
|
import { EnvModel } from '../data/env';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
@@ -17,7 +17,7 @@ export default (app: Router) => {
|
|||||||
try {
|
try {
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const authInfo = await userService.getUserInfo();
|
const authInfo = await userService.getUserInfo();
|
||||||
const envDbContent = getFileContentByName(config.envDbFile);
|
const envCount = await EnvModel.count();
|
||||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||||
|
|
||||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||||
@@ -28,7 +28,7 @@ export default (app: Router) => {
|
|||||||
Object.keys(authInfo).length === 2 &&
|
Object.keys(authInfo).length === 2 &&
|
||||||
authInfo.username === 'admin' &&
|
authInfo.username === 'admin' &&
|
||||||
authInfo.password === 'admin' &&
|
authInfo.password === 'admin' &&
|
||||||
envDbContent.length === 0
|
envCount === 0
|
||||||
) {
|
) {
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,6 @@ export default (app: Router) => {
|
|||||||
const data = await systemService.getLogRemoveFrequency();
|
const data = await systemService.getLogRemoveFrequency();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -76,7 +75,6 @@ export default (app: Router) => {
|
|||||||
);
|
);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -91,7 +89,6 @@ export default (app: Router) => {
|
|||||||
const result = await systemService.checkUpdate();
|
const result = await systemService.checkUpdate();
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -106,7 +103,26 @@ export default (app: Router) => {
|
|||||||
const result = await systemService.updateSystem();
|
const result = await systemService.updateSystem();
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/notify',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
title: Joi.string().required(),
|
||||||
|
content: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.notify(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+32
-15
@@ -1,13 +1,26 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as fs from 'fs';
|
|
||||||
import config from '../config';
|
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import { getFileContentByName } from '../config/util';
|
import multer from 'multer';
|
||||||
|
import path from 'path';
|
||||||
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
import config from '../config';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.uploadPath);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
const ext = path.parse(file.originalname).ext;
|
||||||
|
const key = uuidV4();
|
||||||
|
cb(null, key + ext);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/user', route);
|
app.use('/user', route);
|
||||||
|
|
||||||
@@ -26,7 +39,6 @@ export default (app: Router) => {
|
|||||||
const data = await userService.login({ ...req.body }, req);
|
const data = await userService.login({ ...req.body }, req);
|
||||||
return res.send(data);
|
return res.send(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -41,7 +53,6 @@ export default (app: Router) => {
|
|||||||
await userService.logout(req.platform);
|
await userService.logout(req.platform);
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,7 +73,6 @@ export default (app: Router) => {
|
|||||||
await userService.updateUsernameAndPassword(req.body);
|
await userService.updateUsernameAndPassword(req.body);
|
||||||
res.send({ code: 200, message: '更新成功' });
|
res.send({ code: 200, message: '更新成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -77,6 +87,7 @@ export default (app: Router) => {
|
|||||||
code: 200,
|
code: 200,
|
||||||
data: {
|
data: {
|
||||||
username: authInfo.username,
|
username: authInfo.username,
|
||||||
|
avatar: authInfo.avatar,
|
||||||
twoFactorActivated: authInfo.twoFactorActivated,
|
twoFactorActivated: authInfo.twoFactorActivated,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -95,7 +106,6 @@ export default (app: Router) => {
|
|||||||
const data = await userService.initTwoFactor();
|
const data = await userService.initTwoFactor();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -115,7 +125,6 @@ export default (app: Router) => {
|
|||||||
const data = await userService.activeTwoFactor(req.body.code);
|
const data = await userService.activeTwoFactor(req.body.code);
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -130,7 +139,6 @@ export default (app: Router) => {
|
|||||||
const data = await userService.deactiveTwoFactor();
|
const data = await userService.deactiveTwoFactor();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -152,7 +160,6 @@ export default (app: Router) => {
|
|||||||
const data = await userService.twoFactorLogin(req.body, req);
|
const data = await userService.twoFactorLogin(req.body, req);
|
||||||
res.send(data);
|
res.send(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -167,7 +174,6 @@ export default (app: Router) => {
|
|||||||
const data = await userService.getLoginLog();
|
const data = await userService.getLoginLog();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -182,7 +188,6 @@ export default (app: Router) => {
|
|||||||
const data = await userService.getNotificationMode();
|
const data = await userService.getNotificationMode();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -197,7 +202,6 @@ export default (app: Router) => {
|
|||||||
const result = await userService.updateNotificationMode(req.body);
|
const result = await userService.updateNotificationMode(req.body);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -218,7 +222,6 @@ export default (app: Router) => {
|
|||||||
await userService.updateUsernameAndPassword(req.body);
|
await userService.updateUsernameAndPassword(req.body);
|
||||||
res.send({ code: 200, message: '更新成功' });
|
res.send({ code: 200, message: '更新成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -233,7 +236,21 @@ export default (app: Router) => {
|
|||||||
const result = await userService.updateNotificationMode(req.body);
|
const result = await userService.updateNotificationMode(req.body);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/avatar',
|
||||||
|
upload.single('avatar'),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const result = await userService.updateAvatar(req.file!.filename);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-10
@@ -1,29 +1,23 @@
|
|||||||
import 'reflect-metadata'; // We need this in order to use @Decorators
|
import 'reflect-metadata'; // We need this in order to use @Decorators
|
||||||
|
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
await require('./loaders/initFile').default();
|
await require('./loaders/initFile').default();
|
||||||
|
|
||||||
await require('./loaders/sentry').default({ expressApp: app });
|
await require('./loaders/sentry').default({ expressApp: app });
|
||||||
|
|
||||||
await require('./loaders/db').default();
|
|
||||||
|
|
||||||
await require('./loaders/app').default({ expressApp: app });
|
await require('./loaders/app').default({ expressApp: app });
|
||||||
|
|
||||||
const server = app
|
const server = app
|
||||||
.listen(config.port, () => {
|
.listen(config.port, () => {
|
||||||
Logger.info(`
|
Logger.debug(`✌️ Back server launched on port ${config.port}`);
|
||||||
################################################
|
|
||||||
🛡️ Server listening on port: ${config.port} 🛡️
|
|
||||||
################################################
|
|
||||||
`);
|
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
|||||||
+35
-26
@@ -4,32 +4,42 @@ import { createRandomString } from './util';
|
|||||||
|
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
const lastVersionFile =
|
if (!process.env.QL_DIR) {
|
||||||
'https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
|
// 声明QL_DIR环境变量
|
||||||
|
let qlHomePath = path.join(__dirname, '../../');
|
||||||
|
// 生产环境
|
||||||
|
if (qlHomePath.endsWith('/static/')) {
|
||||||
|
qlHomePath = path.join(qlHomePath, '../');
|
||||||
|
}
|
||||||
|
process.env.QL_DIR = qlHomePath;
|
||||||
|
}
|
||||||
|
|
||||||
const envFound = dotenv.config();
|
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
|
||||||
const rootPath = process.cwd();
|
|
||||||
const envFile = path.join(rootPath, 'config/env.sh');
|
const rootPath = process.env.QL_DIR as string;
|
||||||
const confFile = path.join(rootPath, 'config/config.sh');
|
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||||
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
|
|
||||||
const crontabFile = path.join(rootPath, 'config/crontab.list');
|
const dataPath = path.join(rootPath, 'data/');
|
||||||
const confBakDir = path.join(rootPath, 'config/bak/');
|
|
||||||
const authConfigFile = path.join(rootPath, 'config/auth.json');
|
|
||||||
const extraFile = path.join(rootPath, 'config/extra.sh');
|
|
||||||
const configPath = path.join(rootPath, 'config/');
|
|
||||||
const scriptPath = path.join(rootPath, 'scripts/');
|
|
||||||
const bakPath = path.join(rootPath, 'bak/');
|
|
||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
const logPath = path.join(rootPath, 'log/');
|
const configPath = path.join(dataPath, 'config/');
|
||||||
|
const scriptPath = path.join(dataPath, 'scripts/');
|
||||||
|
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 envFile = path.join(configPath, 'env.sh');
|
||||||
|
const confFile = path.join(configPath, 'config.sh');
|
||||||
|
const crontabFile = path.join(configPath, 'crontab.list');
|
||||||
|
const authConfigFile = path.join(configPath, 'auth.json');
|
||||||
|
const extraFile = path.join(configPath, 'extra.sh');
|
||||||
|
const confBakDir = path.join(dataPath, 'config/bak/');
|
||||||
|
const sampleFile = path.join(samplePath, 'config.sample.sh');
|
||||||
|
const sqliteFile = path.join(samplePath, 'database.sqlite');
|
||||||
|
|
||||||
const authError = '错误的用户名密码,请重试';
|
const authError = '错误的用户名密码,请重试';
|
||||||
const loginFaild = '请先登录!';
|
const loginFaild = '请先登录!';
|
||||||
const configString = 'config sample crontab shareCode diy';
|
const configString = 'config sample crontab shareCode diy';
|
||||||
const dbPath = path.join(rootPath, 'db/');
|
|
||||||
const cronDbFile = path.join(rootPath, 'db/crontab.db');
|
|
||||||
const envDbFile = path.join(rootPath, 'db/env.db');
|
|
||||||
const appDbFile = path.join(rootPath, 'db/app.db');
|
|
||||||
const authDbFile = path.join(rootPath, 'db/auth.db');
|
|
||||||
const dependenceDbFile = path.join(rootPath, 'db/dependence.db');
|
|
||||||
const versionFile = path.join(rootPath, 'src/version.ts');
|
const versionFile = path.join(rootPath, 'src/version.ts');
|
||||||
|
|
||||||
if (envFound.error) {
|
if (envFound.error) {
|
||||||
@@ -39,6 +49,7 @@ if (envFound.error) {
|
|||||||
export default {
|
export default {
|
||||||
port: parseInt(process.env.BACK_PORT as string, 10),
|
port: parseInt(process.env.BACK_PORT as string, 10),
|
||||||
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
||||||
|
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
|
||||||
secret: process.env.SECRET || createRandomString(16, 32),
|
secret: process.env.SECRET || createRandomString(16, 32),
|
||||||
logs: {
|
logs: {
|
||||||
level: process.env.LOG_LEVEL || 'silly',
|
level: process.env.LOG_LEVEL || 'silly',
|
||||||
@@ -59,11 +70,7 @@ export default {
|
|||||||
confFile,
|
confFile,
|
||||||
envFile,
|
envFile,
|
||||||
dbPath,
|
dbPath,
|
||||||
cronDbFile,
|
uploadPath,
|
||||||
envDbFile,
|
|
||||||
appDbFile,
|
|
||||||
authDbFile,
|
|
||||||
dependenceDbFile,
|
|
||||||
configPath,
|
configPath,
|
||||||
scriptPath,
|
scriptPath,
|
||||||
samplePath,
|
samplePath,
|
||||||
@@ -73,6 +80,7 @@ export default {
|
|||||||
'cookie.sh',
|
'cookie.sh',
|
||||||
'crontab.list',
|
'crontab.list',
|
||||||
'env.sh',
|
'env.sh',
|
||||||
|
'token.json',
|
||||||
],
|
],
|
||||||
writePathList: [configPath, scriptPath],
|
writePathList: [configPath, scriptPath],
|
||||||
bakPath,
|
bakPath,
|
||||||
@@ -86,4 +94,5 @@ export default {
|
|||||||
],
|
],
|
||||||
versionFile,
|
versionFile,
|
||||||
lastVersionFile,
|
lastVersionFile,
|
||||||
|
sqliteFile,
|
||||||
};
|
};
|
||||||
|
|||||||
+88
-1
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
import iconv from 'iconv-lite';
|
import iconv from 'iconv-lite';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
|
||||||
export function getFileContentByName(fileName: string) {
|
export function getFileContentByName(fileName: string) {
|
||||||
if (fs.existsSync(fileName)) {
|
if (fs.existsSync(fileName)) {
|
||||||
@@ -107,7 +108,7 @@ export function createRandomString(min: number, max: number): string {
|
|||||||
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOne(arr) {
|
function getOne(arr: any[]) {
|
||||||
return arr[Math.floor(Math.random() * arr.length)];
|
return arr[Math.floor(Math.random() * arr.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +233,14 @@ export async function fileExist(file: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createFile(file: string, data: string = '') {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||||
|
fs.writeFileSync(file, data);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function concurrentRun(
|
export async function concurrentRun(
|
||||||
fnList: Array<() => Promise<any>> = [],
|
fnList: Array<() => Promise<any>> = [],
|
||||||
max = 5,
|
max = 5,
|
||||||
@@ -268,3 +277,81 @@ export async function concurrentRun(
|
|||||||
|
|
||||||
return replyList;
|
return replyList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FileType {
|
||||||
|
'directory',
|
||||||
|
'file',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readDirs(
|
||||||
|
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);
|
||||||
|
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,
|
||||||
|
type: stats.isDirectory() ? 'directory' : 'file',
|
||||||
|
key,
|
||||||
|
parent: relativePath,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function 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));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|||||||
export class Crontab {
|
export class Crontab {
|
||||||
name?: string;
|
name?: string;
|
||||||
command: string;
|
command: string;
|
||||||
schedule: string;
|
schedule?: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
saved?: boolean;
|
saved?: boolean;
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -34,7 +34,7 @@ export class Crontab {
|
|||||||
this.isDisabled = options.isDisabled || 0;
|
this.isDisabled = options.isDisabled || 0;
|
||||||
this.log_path = options.log_path || '';
|
this.log_path = options.log_path || '';
|
||||||
this.isPinned = options.isPinned || 0;
|
this.isPinned = options.isPinned || 0;
|
||||||
this.labels = options.labels || [''];
|
this.labels = options.labels || [];
|
||||||
this.last_running_time = options.last_running_time || 0;
|
this.last_running_time = options.last_running_time || 0;
|
||||||
this.last_execution_time = options.last_execution_time || 0;
|
this.last_execution_time = options.last_execution_time || 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,15 +37,15 @@ export enum DependenceTypes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum InstallDependenceCommandTypes {
|
export enum InstallDependenceCommandTypes {
|
||||||
'npm i -g --force',
|
'pnpm add -g',
|
||||||
'pip3 install',
|
'pip3 install',
|
||||||
'apk add --no-cache -f',
|
'apk add',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum unInstallDependenceCommandTypes {
|
export enum unInstallDependenceCommandTypes {
|
||||||
'npm uninstall -g --force',
|
'pnpm remove -g',
|
||||||
'pip3 uninstall -y',
|
'pip3 uninstall -y',
|
||||||
'apk del -f',
|
'apk del',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DependenceInstance
|
interface DependenceInstance
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export enum NotificationMode {
|
|||||||
'gotify' = 'gotify',
|
'gotify' = 'gotify',
|
||||||
'goCqHttpBot' = 'goCqHttpBot',
|
'goCqHttpBot' = 'goCqHttpBot',
|
||||||
'serverChan' = 'serverChan',
|
'serverChan' = 'serverChan',
|
||||||
|
'pushDeer' = 'pushDeer',
|
||||||
'bark' = 'bark',
|
'bark' = 'bark',
|
||||||
'telegramBot' = 'telegramBot',
|
'telegramBot' = 'telegramBot',
|
||||||
'dingtalkBot' = 'dingtalkBot',
|
'dingtalkBot' = 'dingtalkBot',
|
||||||
@@ -32,8 +33,13 @@ export class ServerChanNotification extends NotificationBaseInfo {
|
|||||||
public serverChanKey = '';
|
public serverChanKey = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PushDeerNotification extends NotificationBaseInfo {
|
||||||
|
public pushDeerKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
export class BarkNotification extends NotificationBaseInfo {
|
export class BarkNotification extends NotificationBaseInfo {
|
||||||
public barkPush = '';
|
public barkPush = '';
|
||||||
|
public barkIcon = 'http://qn.whyour.cn/logo.png';
|
||||||
public barkSound = '';
|
public barkSound = '';
|
||||||
public barkGroup = 'qinglong';
|
public barkGroup = 'qinglong';
|
||||||
}
|
}
|
||||||
@@ -79,6 +85,7 @@ export interface NotificationInfo
|
|||||||
extends GoCqHttpBotNotification,
|
extends GoCqHttpBotNotification,
|
||||||
GotifyNotification,
|
GotifyNotification,
|
||||||
ServerChanNotification,
|
ServerChanNotification,
|
||||||
|
PushDeerNotification,
|
||||||
BarkNotification,
|
BarkNotification,
|
||||||
TelegramBotNotification,
|
TelegramBotNotification,
|
||||||
DingtalkBotNotification,
|
DingtalkBotNotification,
|
||||||
|
|||||||
+3
-1
@@ -13,5 +13,7 @@ export class SockMessage {
|
|||||||
export type SockMessageType =
|
export type SockMessageType =
|
||||||
| 'ping'
|
| 'ping'
|
||||||
| 'installDependence'
|
| 'installDependence'
|
||||||
|
| 'uninstallDependence'
|
||||||
| 'updateSystemVersion'
|
| 'updateSystemVersion'
|
||||||
| 'manuallyRunScript';
|
| 'manuallyRunScript'
|
||||||
|
| 'runSubscriptionEnd';
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { sequelize } from '.';
|
||||||
|
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||||
|
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
||||||
|
|
||||||
|
type SimpleIntervalScheduleUnit = keyof SimpleIntervalSchedule;
|
||||||
|
export class Subscription {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
type?: 'public-repo' | 'private-repo' | 'file';
|
||||||
|
schedule_type?: 'crontab' | 'interval';
|
||||||
|
schedule?: string;
|
||||||
|
interval_schedule?: { type: SimpleIntervalScheduleUnit; value: number };
|
||||||
|
url?: string;
|
||||||
|
whitelist?: string;
|
||||||
|
blacklist?: string;
|
||||||
|
dependences?: string;
|
||||||
|
branch?: string;
|
||||||
|
status?: SubscriptionStatus;
|
||||||
|
pull_type?: 'ssh-key' | 'user-pwd';
|
||||||
|
pull_option?:
|
||||||
|
| { private_key: string }
|
||||||
|
| { username: string; password: string };
|
||||||
|
pid?: number;
|
||||||
|
is_disabled?: 1 | 0;
|
||||||
|
log_path?: string;
|
||||||
|
alias: string;
|
||||||
|
command?: string;
|
||||||
|
extensions?: string;
|
||||||
|
sub_before?: string;
|
||||||
|
sub_after?: string;
|
||||||
|
|
||||||
|
constructor(options: Subscription) {
|
||||||
|
this.id = options.id;
|
||||||
|
this.name = options.name || options.alias;
|
||||||
|
this.type = options.type;
|
||||||
|
this.schedule = options.schedule;
|
||||||
|
this.status =
|
||||||
|
options.status && SubscriptionStatus[options.status]
|
||||||
|
? options.status
|
||||||
|
: SubscriptionStatus.idle;
|
||||||
|
this.url = options.url;
|
||||||
|
this.whitelist = options.whitelist;
|
||||||
|
this.blacklist = options.blacklist;
|
||||||
|
this.dependences = options.dependences;
|
||||||
|
this.branch = options.branch;
|
||||||
|
this.pull_type = options.pull_type;
|
||||||
|
this.pull_option = options.pull_option;
|
||||||
|
this.pid = options.pid;
|
||||||
|
this.is_disabled = options.is_disabled;
|
||||||
|
this.log_path = options.log_path;
|
||||||
|
this.schedule_type = options.schedule_type;
|
||||||
|
this.alias = options.alias;
|
||||||
|
this.interval_schedule = options.interval_schedule;
|
||||||
|
this.extensions = options.extensions;
|
||||||
|
this.sub_before = options.sub_before;
|
||||||
|
this.sub_after = options.sub_after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SubscriptionStatus {
|
||||||
|
'running',
|
||||||
|
'idle',
|
||||||
|
'disabled',
|
||||||
|
'queued',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubscriptionInstance
|
||||||
|
extends Model<Subscription, Subscription>,
|
||||||
|
Subscription {}
|
||||||
|
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
||||||
|
'Subscription',
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
unique: 'compositeIndex',
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
unique: 'compositeIndex',
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
schedule: {
|
||||||
|
unique: 'compositeIndex',
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
interval_schedule: {
|
||||||
|
unique: 'compositeIndex',
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
},
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
whitelist: DataTypes.STRING,
|
||||||
|
blacklist: DataTypes.STRING,
|
||||||
|
status: DataTypes.NUMBER,
|
||||||
|
dependences: DataTypes.STRING,
|
||||||
|
extensions: DataTypes.STRING,
|
||||||
|
sub_before: DataTypes.STRING,
|
||||||
|
sub_after: DataTypes.STRING,
|
||||||
|
branch: DataTypes.STRING,
|
||||||
|
pull_type: DataTypes.STRING,
|
||||||
|
pull_option: DataTypes.JSON,
|
||||||
|
pid: DataTypes.NUMBER,
|
||||||
|
is_disabled: DataTypes.NUMBER,
|
||||||
|
log_path: DataTypes.STRING,
|
||||||
|
schedule_type: DataTypes.STRING,
|
||||||
|
alias: { type: DataTypes.STRING, unique: 'alias' },
|
||||||
|
},
|
||||||
|
);
|
||||||
+7
-5
@@ -1,14 +1,13 @@
|
|||||||
import expressLoader from './express';
|
import expressLoader from './express';
|
||||||
import dependencyInjectorLoader from './dependencyInjector';
|
import depInjectorLoader from './depInjector';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import initData from './initData';
|
import initData from './initData';
|
||||||
import { Application } from 'express';
|
import { Application } from 'express';
|
||||||
import linkDeps from './deps';
|
import linkDeps from './deps';
|
||||||
|
import initTask from './initTask';
|
||||||
|
|
||||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||||
await dependencyInjectorLoader({
|
await depInjectorLoader();
|
||||||
models: [],
|
|
||||||
});
|
|
||||||
Logger.info('✌️ Dependency Injector loaded');
|
Logger.info('✌️ Dependency Injector loaded');
|
||||||
|
|
||||||
await expressLoader({ app: expressApp });
|
await expressLoader({ app: expressApp });
|
||||||
@@ -18,5 +17,8 @@ export default async ({ expressApp }: { expressApp: Application }) => {
|
|||||||
Logger.info('✌️ init data loaded');
|
Logger.info('✌️ init data loaded');
|
||||||
|
|
||||||
await linkDeps();
|
await linkDeps();
|
||||||
Logger.info('✌️ link deps');
|
Logger.info('✌️ link deps loaded');
|
||||||
|
|
||||||
|
initTask();
|
||||||
|
Logger.info('✌️ init task loaded');
|
||||||
};
|
};
|
||||||
|
|||||||
+28
-18
@@ -1,33 +1,43 @@
|
|||||||
import DataStore from 'nedb';
|
|
||||||
import config from '../config';
|
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { fileExist } from '../config/util';
|
import path from 'path';
|
||||||
|
import DataStore from 'nedb';
|
||||||
import { EnvModel } from '../data/env';
|
import { EnvModel } from '../data/env';
|
||||||
import { CrontabModel } from '../data/cron';
|
import { CrontabModel } from '../data/cron';
|
||||||
import { DependenceModel } from '../data/dependence';
|
import { DependenceModel } from '../data/dependence';
|
||||||
import { AppModel } from '../data/open';
|
import { AppModel } from '../data/open';
|
||||||
import { AuthModel } from '../data/auth';
|
import { AuthModel } from '../data/auth';
|
||||||
import { sequelize } from '../data';
|
import { sequelize } from '../data';
|
||||||
|
import { fileExist } from '../config/util';
|
||||||
|
import { SubscriptionModel } from '../data/subscription';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
try {
|
try {
|
||||||
await sequelize.sync();
|
await CrontabModel.sync();
|
||||||
await new Promise((resolve) => setTimeout(() => resolve(null), 5000));
|
await DependenceModel.sync();
|
||||||
|
await AppModel.sync();
|
||||||
|
await AuthModel.sync();
|
||||||
|
await EnvModel.sync();
|
||||||
|
await SubscriptionModel.sync();
|
||||||
|
|
||||||
// try {
|
// try {
|
||||||
// const queryInterface = sequelize.getQueryInterface();
|
// const queryInterface = sequelize.getQueryInterface();
|
||||||
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
|
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
|
||||||
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
|
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
|
||||||
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
|
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
|
||||||
// } catch (error) {
|
// } catch (error) { }
|
||||||
|
|
||||||
// }
|
// 2.10-2.11 升级
|
||||||
|
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
||||||
const crondbExist = await fileExist(config.cronDbFile);
|
const envDbFile = path.join(config.rootPath, 'db/env.db');
|
||||||
const dependenceDbExist = await fileExist(config.dependenceDbFile);
|
const appDbFile = path.join(config.rootPath, 'db/app.db');
|
||||||
const envDbExist = await fileExist(config.envDbFile);
|
const authDbFile = path.join(config.rootPath, 'db/auth.db');
|
||||||
const appDbExist = await fileExist(config.appDbFile);
|
const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
|
||||||
const authDbExist = await fileExist(config.authDbFile);
|
const crondbExist = await fileExist(cronDbFile);
|
||||||
|
const dependenceDbExist = await fileExist(dependenceDbFile);
|
||||||
|
const envDbExist = await fileExist(envDbFile);
|
||||||
|
const appDbExist = await fileExist(appDbFile);
|
||||||
|
const authDbExist = await fileExist(authDbFile);
|
||||||
|
|
||||||
const cronCount = await CrontabModel.count();
|
const cronCount = await CrontabModel.count();
|
||||||
const dependenceCount = await DependenceModel.count();
|
const dependenceCount = await DependenceModel.count();
|
||||||
@@ -36,7 +46,7 @@ export default async () => {
|
|||||||
const authCount = await AuthModel.count();
|
const authCount = await AuthModel.count();
|
||||||
if (crondbExist && cronCount === 0) {
|
if (crondbExist && cronCount === 0) {
|
||||||
const cronDb = new DataStore({
|
const cronDb = new DataStore({
|
||||||
filename: config.cronDbFile,
|
filename: cronDbFile,
|
||||||
autoload: true,
|
autoload: true,
|
||||||
});
|
});
|
||||||
cronDb.persistence.compactDatafile();
|
cronDb.persistence.compactDatafile();
|
||||||
@@ -47,7 +57,7 @@ export default async () => {
|
|||||||
|
|
||||||
if (dependenceDbExist && dependenceCount === 0) {
|
if (dependenceDbExist && dependenceCount === 0) {
|
||||||
const dependenceDb = new DataStore({
|
const dependenceDb = new DataStore({
|
||||||
filename: config.dependenceDbFile,
|
filename: dependenceDbFile,
|
||||||
autoload: true,
|
autoload: true,
|
||||||
});
|
});
|
||||||
dependenceDb.persistence.compactDatafile();
|
dependenceDb.persistence.compactDatafile();
|
||||||
@@ -58,7 +68,7 @@ export default async () => {
|
|||||||
|
|
||||||
if (envDbExist && envCount === 0) {
|
if (envDbExist && envCount === 0) {
|
||||||
const envDb = new DataStore({
|
const envDb = new DataStore({
|
||||||
filename: config.envDbFile,
|
filename: envDbFile,
|
||||||
autoload: true,
|
autoload: true,
|
||||||
});
|
});
|
||||||
envDb.persistence.compactDatafile();
|
envDb.persistence.compactDatafile();
|
||||||
@@ -69,7 +79,7 @@ export default async () => {
|
|||||||
|
|
||||||
if (appDbExist && appCount === 0) {
|
if (appDbExist && appCount === 0) {
|
||||||
const appDb = new DataStore({
|
const appDb = new DataStore({
|
||||||
filename: config.appDbFile,
|
filename: appDbFile,
|
||||||
autoload: true,
|
autoload: true,
|
||||||
});
|
});
|
||||||
appDb.persistence.compactDatafile();
|
appDb.persistence.compactDatafile();
|
||||||
@@ -80,7 +90,7 @@ export default async () => {
|
|||||||
|
|
||||||
if (authDbExist && authCount === 0) {
|
if (authDbExist && authCount === 0) {
|
||||||
const authDb = new DataStore({
|
const authDb = new DataStore({
|
||||||
filename: config.authDbFile,
|
filename: authDbFile,
|
||||||
autoload: true,
|
autoload: true,
|
||||||
});
|
});
|
||||||
authDb.persistence.compactDatafile();
|
authDb.persistence.compactDatafile();
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import LoggerInstance from './logger';
|
import LoggerInstance from './logger';
|
||||||
|
|
||||||
export default ({ models }: { models: { name: string; model: any }[] }) => {
|
export default () => {
|
||||||
try {
|
try {
|
||||||
models.forEach((m) => {
|
|
||||||
Container.set(m.name, m.model);
|
|
||||||
});
|
|
||||||
|
|
||||||
Container.set('logger', LoggerInstance);
|
Container.set('logger', LoggerInstance);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
|
LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
|
||||||
@@ -2,6 +2,7 @@ import path from 'path';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import chokidar from 'chokidar';
|
import chokidar from 'chokidar';
|
||||||
import config from '../config/index';
|
import config from '../config/index';
|
||||||
|
import { promiseExec } from '../config/util';
|
||||||
|
|
||||||
function linkToNodeModule(src: string, dst?: string) {
|
function linkToNodeModule(src: string, dst?: string) {
|
||||||
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
||||||
@@ -16,7 +17,32 @@ function linkToNodeModule(src: string, dst?: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function linkCommand() {
|
||||||
|
const commandPath = await promiseExec('which node');
|
||||||
|
const commandDir = path.dirname(commandPath);
|
||||||
|
const linkShell = [
|
||||||
|
{
|
||||||
|
src: 'update.sh',
|
||||||
|
dest: 'ql',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'task.sh',
|
||||||
|
dest: 'task',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const link of linkShell) {
|
||||||
|
const source = path.join(config.rootPath, 'shell', link.src);
|
||||||
|
const target = path.join(commandDir, link.dest);
|
||||||
|
if (fs.existsSync(target)) {
|
||||||
|
fs.unlinkSync(target);
|
||||||
|
}
|
||||||
|
fs.symlink(source, target, (err) => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async (src: string = 'deps') => {
|
export default async (src: string = 'deps') => {
|
||||||
|
await linkCommand();
|
||||||
linkToNodeModule(src);
|
linkToNodeModule(src);
|
||||||
|
|
||||||
const source = path.join(config.rootPath, src);
|
const source = path.join(config.rootPath, src);
|
||||||
|
|||||||
+32
-13
@@ -1,28 +1,31 @@
|
|||||||
import { Request, Response, NextFunction, Application } from 'express';
|
import express, { Request, Response, NextFunction, Application } from 'express';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import routes from '../api';
|
import routes from '../api';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import jwt from 'express-jwt';
|
import jwt from 'express-jwt';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { getFileContentByName, getPlatform, getToken } from '../config/util';
|
import { getPlatform, getToken } from '../config/util';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
import OpenService from '../services/open';
|
import OpenService from '../services/open';
|
||||||
import rewrite from 'express-urlrewrite';
|
import rewrite from 'express-urlrewrite';
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
import handler from 'serve-handler';
|
import handler from 'serve-handler';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { EnvModel } from '../data/env';
|
||||||
|
import { errors } from 'celebrate';
|
||||||
|
|
||||||
export default ({ app }: { app: Application }) => {
|
export default ({ app }: { app: Application }) => {
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
return handler(req, res, {
|
return handler(req, res, {
|
||||||
public: 'dist',
|
public: 'static/dist',
|
||||||
rewrites: [{ source: '**', destination: '/index.html' }],
|
rewrites: [{ source: '**', destination: '/index.html' }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -77,13 +80,6 @@ export default ({ app }: { app: Application }) => {
|
|||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const remoteAddress = req.socket.remoteAddress;
|
|
||||||
if (
|
|
||||||
remoteAddress === '::ffff:127.0.0.1' &&
|
|
||||||
originPath === '/api/crons/status'
|
|
||||||
) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -104,14 +100,14 @@ export default ({ app }: { app: Application }) => {
|
|||||||
}
|
}
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const authInfo = await userService.getUserInfo();
|
const authInfo = await userService.getUserInfo();
|
||||||
const envDbContent = getFileContentByName(config.envDbFile);
|
const envCount = await EnvModel.count();
|
||||||
|
|
||||||
let isInitialized = true;
|
let isInitialized = true;
|
||||||
if (
|
if (
|
||||||
Object.keys(authInfo).length === 2 &&
|
Object.keys(authInfo).length === 2 &&
|
||||||
authInfo.username === 'admin' &&
|
authInfo.username === 'admin' &&
|
||||||
authInfo.password === 'admin' &&
|
authInfo.password === 'admin' &&
|
||||||
envDbContent.length === 0
|
envCount === 0
|
||||||
) {
|
) {
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
}
|
}
|
||||||
@@ -132,7 +128,7 @@ export default ({ app }: { app: Application }) => {
|
|||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(Sentry.Handlers.errorHandler());
|
app.use(errors());
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
@@ -151,6 +147,29 @@ export default ({ app }: { app: Application }) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
(
|
||||||
|
err: Error & { errors: any[] },
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) => {
|
||||||
|
if (err.name.includes('Sequelize')) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.send({
|
||||||
|
code: 400,
|
||||||
|
message: `${err.name} ${err.message}`,
|
||||||
|
validation: err.errors,
|
||||||
|
})
|
||||||
|
.end();
|
||||||
|
}
|
||||||
|
return next(err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(Sentry.Handlers.errorHandler());
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
err: Error & { status: number },
|
err: Error & { status: number },
|
||||||
|
|||||||
+69
-24
@@ -7,6 +7,7 @@ import EnvService from '../services/env';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { DependenceModel } from '../data/dependence';
|
import { DependenceModel } from '../data/dependence';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
@@ -20,16 +21,17 @@ export default async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||||
DependenceModel.findAll({ where: {} }).then(async (docs) => {
|
DependenceModel.findAll({
|
||||||
|
where: {},
|
||||||
|
order: [['type', 'DESC']],
|
||||||
|
raw: true,
|
||||||
|
}).then(async (docs) => {
|
||||||
const groups = _.groupBy(docs, 'type');
|
const groups = _.groupBy(docs, 'type');
|
||||||
for (const key in groups) {
|
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
|
||||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
for (const key of keys) {
|
||||||
const group = groups[key];
|
const group = groups[key];
|
||||||
const depIds = group.map((x) => x.id);
|
const depIds = group.map((x) => x.id);
|
||||||
for (const dep of depIds) {
|
await dependenceService.reInstall(depIds as number[]);
|
||||||
await dependenceService.reInstall([dep]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@ export default async () => {
|
|||||||
where: {
|
where: {
|
||||||
isDisabled: { [Op.ne]: 1 },
|
isDisabled: { [Op.ne]: 1 },
|
||||||
command: {
|
command: {
|
||||||
[Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%$ql raw%` }],
|
[Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%ql raw%` }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).then((docs) => {
|
}).then((docs) => {
|
||||||
@@ -50,23 +52,66 @@ export default async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 更新2.11.3以前的脚本路径
|
||||||
|
CrontabModel.findAll({
|
||||||
|
where: {
|
||||||
|
command: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ [Op.like]: `%\/${config.rootPath}\/scripts\/%` },
|
||||||
|
{ [Op.like]: `%\/${config.rootPath}\/config\/%` },
|
||||||
|
{ [Op.like]: `%\/${config.rootPath}\/log\/%` },
|
||||||
|
{ [Op.like]: `%\/${config.rootPath}\/db\/%` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).then(async (docs) => {
|
||||||
|
for (let i = 0; i < docs.length; i++) {
|
||||||
|
const doc = docs[i];
|
||||||
|
if (doc) {
|
||||||
|
if (doc.command.includes(`${config.rootPath}/scripts/`)) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ command: doc.command.replace(`${config.rootPath}/scripts/`, '') },
|
||||||
|
{ where: { id: doc.id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (doc.command.includes(`${config.rootPath}/log/`)) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{
|
||||||
|
command: `${config.rootPath}/data/log/${doc.command.replace(
|
||||||
|
`${config.rootPath}/log/`,
|
||||||
|
'',
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
{ where: { id: doc.id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (doc.command.includes(`${config.rootPath}/config/`)) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{
|
||||||
|
command: `${config.rootPath}/data/config/${doc.command.replace(
|
||||||
|
`${config.rootPath}/config/`,
|
||||||
|
'',
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
{ where: { id: doc.id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (doc.command.includes(`${config.rootPath}/db/`)) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{
|
||||||
|
command: `${config.rootPath}/data/db/${doc.command.replace(
|
||||||
|
`${config.rootPath}/db/`,
|
||||||
|
'',
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
{ where: { id: doc.id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化保存一次ck和定时任务数据
|
// 初始化保存一次ck和定时任务数据
|
||||||
await cronService.autosave_crontab();
|
await cronService.autosave_crontab();
|
||||||
await envService.set_envs();
|
await envService.set_envs();
|
||||||
};
|
};
|
||||||
|
|
||||||
function randomSchedule(from: number, to: number) {
|
|
||||||
const result: any[] = [];
|
|
||||||
const arr = [...Array(from).keys()];
|
|
||||||
let count = arr.length;
|
|
||||||
for (let i = 0; i < to; i++) {
|
|
||||||
const index = ~~(Math.random() * count) + i;
|
|
||||||
if (result.includes(arr[index])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
result[i] = arr[index];
|
|
||||||
arr[index] = arr[i];
|
|
||||||
count--;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { fileExist } from '../config/util';
|
import { fileExist } from '../config/util';
|
||||||
|
|
||||||
const rootPath = process.cwd();
|
const rootPath = process.env.QL_DIR as string;;
|
||||||
const confFile = path.join(rootPath, 'config/config.sh');
|
const dataPath = path.join(rootPath, 'data/');
|
||||||
const sampleConfigFile = path.join(rootPath, 'sample/config.sample.sh');
|
const configPath = path.join(dataPath, 'config/');
|
||||||
const sampleAuthFile = path.join(rootPath, 'sample/auth.sample.json');
|
const scriptPath = path.join(dataPath, 'scripts/');
|
||||||
const authConfigFile = path.join(rootPath, 'config/auth.json');
|
const logPath = path.join(dataPath, 'log/');
|
||||||
const configPath = path.join(rootPath, 'config/');
|
const uploadPath = path.join(dataPath, 'upload/');
|
||||||
const scriptPath = path.join(rootPath, 'scripts/');
|
const bakPath = path.join(dataPath, 'bak/');
|
||||||
const logPath = path.join(rootPath, 'log/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
|
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 homedir = os.homedir();
|
||||||
|
const sshPath = path.resolve(homedir, '.ssh');
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const authFileExist = await fileExist(authConfigFile);
|
const authFileExist = await fileExist(authConfigFile);
|
||||||
@@ -19,6 +26,9 @@ export default async () => {
|
|||||||
const scriptDirExist = await fileExist(scriptPath);
|
const scriptDirExist = await fileExist(scriptPath);
|
||||||
const logDirExist = await fileExist(logPath);
|
const logDirExist = await fileExist(logPath);
|
||||||
const configDirExist = await fileExist(configPath);
|
const configDirExist = await fileExist(configPath);
|
||||||
|
const uploadDirExist = await fileExist(uploadPath);
|
||||||
|
const sshDirExist = await fileExist(sshPath);
|
||||||
|
const bakDirExist = await fileExist(bakPath);
|
||||||
|
|
||||||
if (!configDirExist) {
|
if (!configDirExist) {
|
||||||
fs.mkdirSync(configPath);
|
fs.mkdirSync(configPath);
|
||||||
@@ -40,6 +50,18 @@ export default async () => {
|
|||||||
fs.mkdirSync(logPath);
|
fs.mkdirSync(logPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!uploadDirExist) {
|
||||||
|
fs.mkdirSync(uploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sshDirExist) {
|
||||||
|
fs.mkdirSync(sshPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bakDirExist) {
|
||||||
|
fs.mkdirSync(bakPath);
|
||||||
|
}
|
||||||
|
|
||||||
dotenv.config({ path: confFile });
|
dotenv.config({ path: confFile });
|
||||||
|
|
||||||
Logger.info('✌️ Init file down');
|
Logger.info('✌️ Init file down');
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Container } from 'typedi';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import SystemService from '../services/system';
|
||||||
|
import ScheduleService from '../services/schedule';
|
||||||
|
import SubscriptionService from '../services/subscription';
|
||||||
|
import config from '../config';
|
||||||
|
import { fileExist } from '../config/util';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const scheduleService = Container.get(ScheduleService);
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
|
||||||
|
// 生成内置token
|
||||||
|
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
|
||||||
|
const tokenFile = `${config.rootPath}/static/build/token.js`;
|
||||||
|
if (await fileExist(tokenFile)) {
|
||||||
|
tokenCommand = `node ${tokenFile}`;
|
||||||
|
}
|
||||||
|
const cron = {
|
||||||
|
id: 'token',
|
||||||
|
name: '生成token',
|
||||||
|
command: tokenCommand,
|
||||||
|
};
|
||||||
|
scheduleService.createIntervalTask(cron as any, {
|
||||||
|
days: 28,
|
||||||
|
runImmediately: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 运行删除日志任务
|
||||||
|
const data = await systemService.getLogRemoveFrequency();
|
||||||
|
if (data && data.info && data.info.frequency) {
|
||||||
|
const cron = {
|
||||||
|
id: data.id,
|
||||||
|
name: '删除日志',
|
||||||
|
command: `ql rmlog ${data.info.frequency}`,
|
||||||
|
};
|
||||||
|
scheduleService.createIntervalTask(cron, {
|
||||||
|
days: data.info.frequency,
|
||||||
|
runImmediately: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行所有订阅
|
||||||
|
const subs = await subscriptionService.list();
|
||||||
|
for (const sub of subs) {
|
||||||
|
await subscriptionService.handleTask(
|
||||||
|
sub,
|
||||||
|
!sub.is_disabled,
|
||||||
|
true,
|
||||||
|
!sub.is_disabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import Logger from './loaders/logger';
|
||||||
|
import config from './config';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
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.send({ code: 200, data: stdout });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app
|
||||||
|
.listen(config.publicPort, async () => {
|
||||||
|
await require('./loaders/sentry').default({ expressApp: app });
|
||||||
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
|
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`);
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
Logger.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+100
-21
@@ -9,6 +9,8 @@ import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
|
|||||||
import { promises, existsSync } from 'fs';
|
import { promises, existsSync } from 'fs';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
|
import path from 'path';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
@@ -16,7 +18,7 @@ export default class CronService {
|
|||||||
|
|
||||||
private isSixCron(cron: Crontab) {
|
private isSixCron(cron: Crontab) {
|
||||||
const { schedule } = cron;
|
const { schedule } = cron;
|
||||||
if (schedule.split(/ +/).length === 6) {
|
if (schedule?.split(/ +/).length === 6) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -194,14 +196,23 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const err = await this.killTask(doc.command);
|
const err = await this.killTask(doc.command);
|
||||||
const logFileExist = await fileExist(doc.log_path);
|
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||||
if (doc.log_path && logFileExist) {
|
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}` : '';
|
const str = err ? `\n${err}` : '';
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
`${doc.log_path}`,
|
`${absolutePath}`,
|
||||||
`${str}\n## 执行结束... ${new Date()
|
`${str}\n## 执行结束... ${endTime.format(
|
||||||
.toLocaleString('zh', { hour12: false })
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
.replace(' 24:', ' 00:')} `,
|
)}${diffTimeStr}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,6 +240,7 @@ export default class CronService {
|
|||||||
pids = pids.slice(0, 3);
|
pids = pids.slice(0, 3);
|
||||||
for (const id of pids) {
|
for (const id of pids) {
|
||||||
const c = `kill -9 ${id.slice(1)}`;
|
const c = `kill -9 ${id.slice(1)}`;
|
||||||
|
try {
|
||||||
const { stdout, stderr } = await execAsync(c);
|
const { stdout, stderr } = await execAsync(c);
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
killLogs.push(stderr);
|
killLogs.push(stderr);
|
||||||
@@ -236,6 +248,9 @@ export default class CronService {
|
|||||||
if (stdout) {
|
if (stdout) {
|
||||||
killLogs.push(stdout);
|
killLogs.push(stdout);
|
||||||
}
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
killLogs.push(error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
||||||
@@ -253,6 +268,8 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { id, command, log_path } = cron;
|
let { id, command, log_path } = cron;
|
||||||
|
const absolutePath = path.resolve(config.logPath, `${log_path}`);
|
||||||
|
const logFileExist = log_path && (await fileExist(absolutePath));
|
||||||
|
|
||||||
this.logger.silly('Running job');
|
this.logger.silly('Running job');
|
||||||
this.logger.silly('ID: ' + id);
|
this.logger.silly('ID: ' + id);
|
||||||
@@ -273,13 +290,13 @@ export default class CronService {
|
|||||||
{ where: { id } },
|
{ where: { id } },
|
||||||
);
|
);
|
||||||
cp.stderr.on('data', (data) => {
|
cp.stderr.on('data', (data) => {
|
||||||
if (log_path) {
|
if (logFileExist) {
|
||||||
fs.appendFileSync(`${log_path}`, `${data}`);
|
fs.appendFileSync(`${absolutePath}`, `${data}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cp.on('error', (err) => {
|
cp.on('error', (err) => {
|
||||||
if (log_path) {
|
if (logFileExist) {
|
||||||
fs.appendFileSync(`${log_path}`, `${JSON.stringify(err)}`);
|
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -320,8 +337,10 @@ export default class CronService {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.log_path) {
|
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||||
return getFileContentByName(`${doc.log_path}`);
|
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||||
|
if (logFileExist) {
|
||||||
|
return getFileContentByName(`${absolutePath}`);
|
||||||
}
|
}
|
||||||
const [, commandStr, url] = doc.command.split(/ +/);
|
const [, commandStr, url] = doc.command.split(/ +/);
|
||||||
let logPath = this.getKey(commandStr);
|
let logPath = this.getKey(commandStr);
|
||||||
@@ -344,15 +363,75 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getKey(command: string) {
|
public async logs(id: number) {
|
||||||
|
const doc = await this.getDb({ id });
|
||||||
|
if (!doc) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.log_path) {
|
||||||
|
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||||
|
const dir = path.resolve(config.logPath, relativeDir);
|
||||||
|
if (existsSync(dir)) {
|
||||||
|
let files = await promises.readdir(dir);
|
||||||
|
return files
|
||||||
|
.map((x) => ({
|
||||||
|
filename: x,
|
||||||
|
directory: relativeDir.replace(config.logPath, ''),
|
||||||
|
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 getKey(command: string): string {
|
||||||
const start =
|
const start =
|
||||||
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
||||||
const end =
|
const end =
|
||||||
command.lastIndexOf('.') !== -1
|
command.lastIndexOf('.') !== -1
|
||||||
? command.lastIndexOf('.')
|
? command.lastIndexOf('.')
|
||||||
: command.length;
|
: command.length;
|
||||||
|
|
||||||
|
const tmpStr = command.substring(0, start - 1);
|
||||||
|
let index = 0;
|
||||||
|
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
|
||||||
|
index = tmpStr.lastIndexOf('/');
|
||||||
|
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
|
||||||
|
index = tmpStr.lastIndexOf(':');
|
||||||
|
}
|
||||||
|
if (index) {
|
||||||
|
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
|
||||||
|
} else {
|
||||||
return command.substring(start, end);
|
return command.substring(start, end);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private make_command(tab: Crontab) {
|
private make_command(tab: Crontab) {
|
||||||
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
||||||
@@ -364,7 +443,7 @@ export default class CronService {
|
|||||||
var crontab_string = '';
|
var crontab_string = '';
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||||
if (tab.isDisabled === 1 || _schedule.length !== 5) {
|
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
|
||||||
crontab_string += '# ';
|
crontab_string += '# ';
|
||||||
crontab_string += tab.schedule;
|
crontab_string += tab.schedule;
|
||||||
crontab_string += ' ';
|
crontab_string += ' ';
|
||||||
@@ -390,22 +469,22 @@ export default class CronService {
|
|||||||
|
|
||||||
public import_crontab() {
|
public import_crontab() {
|
||||||
exec('crontab -l', (error, stdout, stderr) => {
|
exec('crontab -l', (error, stdout, stderr) => {
|
||||||
var lines = stdout.split('\n');
|
const lines = stdout.split('\n');
|
||||||
var namePrefix = new Date().getTime();
|
const namePrefix = new Date().getTime();
|
||||||
|
|
||||||
lines.reverse().forEach(async (line, index) => {
|
lines.reverse().forEach(async (line, index) => {
|
||||||
line = line.replace(/\t+/g, ' ');
|
line = line.replace(/\t+/g, ' ');
|
||||||
var regex =
|
const regex =
|
||||||
/^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
|
/^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
|
||||||
var command = line.replace(regex, '').trim();
|
const command = line.replace(regex, '').trim();
|
||||||
var schedule = line.replace(command, '').trim();
|
const schedule = line.replace(command, '').trim();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
command &&
|
command &&
|
||||||
schedule &&
|
schedule &&
|
||||||
cron_parser.parseExpression(schedule).hasNext()
|
cron_parser.parseExpression(schedule).hasNext()
|
||||||
) {
|
) {
|
||||||
var name = namePrefix + '_' + index;
|
const name = namePrefix + '_' + index;
|
||||||
|
|
||||||
const _crontab = await CrontabModel.findOne({
|
const _crontab = await CrontabModel.findOne({
|
||||||
where: { command, schedule },
|
where: { command, schedule },
|
||||||
|
|||||||
+49
-36
@@ -13,6 +13,8 @@ import _ from 'lodash';
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
|
import { concurrentRun } from '../config/util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class DependenceService {
|
export default class DependenceService {
|
||||||
@@ -27,7 +29,7 @@ export default class DependenceService {
|
|||||||
return tab;
|
return tab;
|
||||||
});
|
});
|
||||||
const docs = await this.insert(tabs);
|
const docs = await this.insert(tabs);
|
||||||
this.installOrUninstallDependencies(docs);
|
this.installDependenceOneByOne(docs);
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ export default class DependenceService {
|
|||||||
status: DependenceStatus.installing,
|
status: DependenceStatus.installing,
|
||||||
});
|
});
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
this.installOrUninstallDependencies([newDoc]);
|
this.installDependenceOneByOne([newDoc]);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,13 +58,13 @@ export default class DependenceService {
|
|||||||
return await this.getDb({ id: payload.id });
|
return await this.getDb({ id: payload.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
||||||
await DependenceModel.update(
|
await DependenceModel.update(
|
||||||
{ status: DependenceStatus.removing, log: [] },
|
{ status: DependenceStatus.removing, log: [] },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
this.installOrUninstallDependencies(docs, false);
|
this.installDependenceOneByOne(docs, false, force);
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +82,7 @@ export default class DependenceService {
|
|||||||
const encodeText = encodeURIComponent(searchValue);
|
const encodeText = encodeURIComponent(searchValue);
|
||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchValue}&` },
|
{ [Op.like]: `%${searchValue}%` },
|
||||||
{ [Op.like]: `%${encodeText}%` },
|
{ [Op.like]: `%${encodeText}%` },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -98,6 +100,20 @@ export default class DependenceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private installDependenceOneByOne(
|
||||||
|
docs: Dependence[],
|
||||||
|
isInstall: boolean = true,
|
||||||
|
force: boolean = false,
|
||||||
|
) {
|
||||||
|
concurrentRun(
|
||||||
|
docs.map(
|
||||||
|
(dep) => async () =>
|
||||||
|
await this.installOrUninstallDependencies([dep], isInstall, force),
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
||||||
await DependenceModel.update(
|
await DependenceModel.update(
|
||||||
{ status: DependenceStatus.installing, log: [] },
|
{ status: DependenceStatus.installing, log: [] },
|
||||||
@@ -105,7 +121,7 @@ export default class DependenceService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
await this.installOrUninstallDependencies(docs);
|
this.installDependenceOneByOne(docs);
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,12 +144,16 @@ export default class DependenceService {
|
|||||||
public installOrUninstallDependencies(
|
public installOrUninstallDependencies(
|
||||||
dependencies: Dependence[],
|
dependencies: Dependence[],
|
||||||
isInstall: boolean = true,
|
isInstall: boolean = true,
|
||||||
|
force: boolean = false,
|
||||||
) {
|
) {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
if (dependencies.length === 0) {
|
if (dependencies.length === 0) {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const socketMessageType = !force
|
||||||
|
? 'installDependence'
|
||||||
|
: 'uninstallDependence';
|
||||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
const depNames = dependencies.map((x) => x.name).join(' ');
|
||||||
const depRunCommand = (
|
const depRunCommand = (
|
||||||
isInstall
|
isInstall
|
||||||
@@ -142,24 +162,23 @@ export default class DependenceService {
|
|||||||
)[dependencies[0].type as any];
|
)[dependencies[0].type as any];
|
||||||
const actionText = isInstall ? '安装' : '删除';
|
const actionText = isInstall ? '安装' : '删除';
|
||||||
const depIds = dependencies.map((x) => x.id) as number[];
|
const depIds = dependencies.map((x) => x.id) as number[];
|
||||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
const startTime = dayjs();
|
||||||
const startTime = Date.now();
|
|
||||||
|
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)}\n\n`;
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'installDependence',
|
type: socketMessageType,
|
||||||
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
message,
|
||||||
startTime,
|
|
||||||
).toLocaleString()}`,
|
|
||||||
references: depIds,
|
references: depIds,
|
||||||
});
|
});
|
||||||
await this.updateLog(
|
await this.updateLog(depIds, message);
|
||||||
depIds,
|
|
||||||
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||||
startTime,
|
|
||||||
).toLocaleString()}\n`,
|
|
||||||
);
|
|
||||||
cp.stdout.on('data', async (data) => {
|
cp.stdout.on('data', async (data) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'installDependence',
|
type: socketMessageType,
|
||||||
message: data.toString(),
|
message: data.toString(),
|
||||||
references: depIds,
|
references: depIds,
|
||||||
});
|
});
|
||||||
@@ -168,7 +187,7 @@ export default class DependenceService {
|
|||||||
|
|
||||||
cp.stderr.on('data', async (data) => {
|
cp.stderr.on('data', async (data) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'installDependence',
|
type: socketMessageType,
|
||||||
message: data.toString(),
|
message: data.toString(),
|
||||||
references: depIds,
|
references: depIds,
|
||||||
});
|
});
|
||||||
@@ -177,7 +196,7 @@ export default class DependenceService {
|
|||||||
|
|
||||||
cp.on('error', async (err) => {
|
cp.on('error', async (err) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'installDependence',
|
type: socketMessageType,
|
||||||
message: JSON.stringify(err),
|
message: JSON.stringify(err),
|
||||||
references: depIds,
|
references: depIds,
|
||||||
});
|
});
|
||||||
@@ -186,23 +205,19 @@ export default class DependenceService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cp.on('close', async (code) => {
|
cp.on('close', async (code) => {
|
||||||
const endTime = Date.now();
|
const endTime = dayjs();
|
||||||
const isSucceed = code === 0;
|
const isSucceed = code === 0;
|
||||||
const resultText = isSucceed ? '成功' : '失败';
|
const resultText = isSucceed ? '成功' : '失败';
|
||||||
|
|
||||||
|
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'installDependence',
|
type: socketMessageType,
|
||||||
message: `依赖${actionText}${resultText},结束时间 ${new Date(
|
message,
|
||||||
endTime,
|
|
||||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
|
||||||
references: depIds,
|
references: depIds,
|
||||||
});
|
});
|
||||||
await this.updateLog(
|
await this.updateLog(depIds, message);
|
||||||
depIds,
|
|
||||||
`依赖${actionText}${resultText},结束时间 ${new Date(
|
|
||||||
endTime,
|
|
||||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
|
||||||
);
|
|
||||||
|
|
||||||
let status = null;
|
let status = null;
|
||||||
if (isSucceed) {
|
if (isSucceed) {
|
||||||
@@ -216,11 +231,9 @@ export default class DependenceService {
|
|||||||
}
|
}
|
||||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||||
|
|
||||||
// 如果删除依赖成功,3秒后删除数据库记录
|
// 如果删除依赖成功或者强制删除
|
||||||
if (isSucceed && !isInstall) {
|
if ((isSucceed || force) && !isInstall) {
|
||||||
setTimeout(() => {
|
|
||||||
this.removeDb(depIds);
|
this.removeDb(depIds);
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default class EnvService {
|
|||||||
|
|
||||||
public async remove(ids: string[]) {
|
public async remove(ids: string[]) {
|
||||||
await EnvModel.destroy({ where: { id: ids } });
|
await EnvModel.destroy({ where: { id: ids } });
|
||||||
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async move(
|
public async move(
|
||||||
@@ -167,13 +168,11 @@ export default class EnvService {
|
|||||||
.filter((x) => x.status !== EnvStatus.disabled)
|
.filter((x) => x.status !== EnvStatus.disabled)
|
||||||
.map('value')
|
.map('value')
|
||||||
.join('&')
|
.join('&')
|
||||||
.replace(/ /g, '');
|
.replace(/(\\)[^n]/g, '\\\\')
|
||||||
if (/"/.test(value)) {
|
.replace(/(\\$)/, '\\\\')
|
||||||
value = `'${value}'`;
|
.replace(/"/g, '\\"')
|
||||||
} else {
|
.trim();
|
||||||
value = `"${value}"`;
|
env_string += `export ${key}="${value}"\n`;
|
||||||
}
|
|
||||||
env_string += `export ${key}=${value}\n`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-7
@@ -16,6 +16,7 @@ export default class NotificationService {
|
|||||||
['gotify', this.gotify],
|
['gotify', this.gotify],
|
||||||
['goCqHttpBot', this.goCqHttpBot],
|
['goCqHttpBot', this.goCqHttpBot],
|
||||||
['serverChan', this.serverChan],
|
['serverChan', this.serverChan],
|
||||||
|
['pushDeer', this.pushDeer],
|
||||||
['bark', this.bark],
|
['bark', this.bark],
|
||||||
['telegramBot', this.telegramBot],
|
['telegramBot', this.telegramBot],
|
||||||
['dingtalkBot', this.dingtalkBot],
|
['dingtalkBot', this.dingtalkBot],
|
||||||
@@ -90,14 +91,12 @@ export default class NotificationService {
|
|||||||
private async goCqHttpBot() {
|
private async goCqHttpBot() {
|
||||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(
|
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||||
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`,
|
|
||||||
{
|
|
||||||
timeout: this.timeout,
|
timeout: this.timeout,
|
||||||
retry: 0,
|
retry: 0,
|
||||||
json: { message: `${this.title}\n${this.content}` },
|
json: { message: `${this.title}\n${this.content}` },
|
||||||
},
|
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||||
)
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.retcode === 0;
|
return res.retcode === 0;
|
||||||
}
|
}
|
||||||
@@ -118,8 +117,26 @@ export default class NotificationService {
|
|||||||
return res.errno === 0 || res.data.errno === 0;
|
return res.errno === 0 || res.data.errno === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async pushDeer() {
|
||||||
|
const { pushDeerKey } = this.params;
|
||||||
|
const url = `https://api2.pushdeer.com/message/push`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
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();
|
||||||
|
return (
|
||||||
|
res.content.result.length !== undefined && res.content.result.length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async bark() {
|
private async bark() {
|
||||||
let { barkPush, barkSound, barkGroup } = this.params;
|
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
||||||
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
||||||
barkPush = `https://api.day.app/${barkPush}`;
|
barkPush = `https://api.day.app/${barkPush}`;
|
||||||
}
|
}
|
||||||
@@ -127,7 +144,7 @@ export default class NotificationService {
|
|||||||
this.title,
|
this.title,
|
||||||
)}/${encodeURIComponent(
|
)}/${encodeURIComponent(
|
||||||
this.content,
|
this.content,
|
||||||
)}?sound=${barkSound}&group=${barkGroup}`;
|
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.get(url, {
|
.get(url, {
|
||||||
timeout: this.timeout,
|
timeout: this.timeout,
|
||||||
|
|||||||
+42
-3
@@ -90,7 +90,9 @@ export default class OpenService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition);
|
const result = await this.find(condition);
|
||||||
return result.map((x) => ({ ...x, tokens: [] }));
|
return result
|
||||||
|
.filter((x) => x.name !== 'system')
|
||||||
|
.map((x) => ({ ...x, tokens: [] }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -108,12 +110,26 @@ export default class OpenService {
|
|||||||
client_id: string;
|
client_id: string;
|
||||||
client_secret: string;
|
client_secret: string;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
const token = uuidV4();
|
let token = uuidV4();
|
||||||
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
|
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
|
||||||
const doc = await AppModel.findOne({ where: { client_id, client_secret } });
|
const doc = await AppModel.findOne({ where: { client_id, client_secret } });
|
||||||
if (doc) {
|
if (doc) {
|
||||||
|
const timestamp = Math.round(Date.now() / 1000);
|
||||||
|
const invalidTokens = (doc.tokens || []).filter(
|
||||||
|
(x) => x.expiration >= timestamp,
|
||||||
|
);
|
||||||
|
let tokens = invalidTokens;
|
||||||
|
if (invalidTokens.length > 5) {
|
||||||
|
tokens = [
|
||||||
|
...invalidTokens.slice(0, 4),
|
||||||
|
{ ...invalidTokens[4], expiration },
|
||||||
|
];
|
||||||
|
token = invalidTokens[4].value;
|
||||||
|
} else {
|
||||||
|
tokens = [...invalidTokens, { value: token, expiration }];
|
||||||
|
}
|
||||||
await AppModel.update(
|
await AppModel.update(
|
||||||
{ tokens: [...(doc.tokens || []), { value: token, expiration }] },
|
{ tokens },
|
||||||
{ where: { client_id, client_secret } },
|
{ where: { client_id, client_secret } },
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -128,4 +144,27 @@ export default class OpenService {
|
|||||||
return { code: 400, message: 'client_id或client_seret有误' };
|
return { code: 400, message: 'client_id或client_seret有误' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async generateSystemToken(): Promise<{
|
||||||
|
value: string;
|
||||||
|
expiration: number;
|
||||||
|
}> {
|
||||||
|
let systemApp = (await AppModel.findOne({
|
||||||
|
where: { name: 'system' },
|
||||||
|
})) as App;
|
||||||
|
if (!systemApp) {
|
||||||
|
systemApp = await this.create({
|
||||||
|
name: 'system',
|
||||||
|
scopes: ['crons', 'system'],
|
||||||
|
} as App);
|
||||||
|
}
|
||||||
|
const { data } = await this.authToken({
|
||||||
|
client_id: systemApp.client_id,
|
||||||
|
client_secret: systemApp.client_secret,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
value: data.token,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+170
-39
@@ -1,62 +1,193 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import nodeSchedule from 'node-schedule';
|
import nodeSchedule from 'node-schedule';
|
||||||
import { Crontab } from '../data/cron';
|
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
|
||||||
import { exec } from 'child_process';
|
import {
|
||||||
|
ToadScheduler,
|
||||||
|
LongIntervalJob,
|
||||||
|
AsyncTask,
|
||||||
|
SimpleIntervalSchedule,
|
||||||
|
} from 'toad-scheduler';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
interface ScheduleTaskType {
|
||||||
|
id: number;
|
||||||
|
command: string;
|
||||||
|
name?: string;
|
||||||
|
schedule?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskCallbacks {
|
||||||
|
onBefore?: (startTime: dayjs.Dayjs) => Promise<void>;
|
||||||
|
onStart?: (
|
||||||
|
cp: ChildProcessWithoutNullStreams,
|
||||||
|
startTime: dayjs.Dayjs,
|
||||||
|
) => Promise<void>;
|
||||||
|
onEnd?: (
|
||||||
|
cp: ChildProcessWithoutNullStreams,
|
||||||
|
endTime: dayjs.Dayjs,
|
||||||
|
diff: number,
|
||||||
|
) => Promise<void>;
|
||||||
|
onLog?: (message: string) => Promise<void>;
|
||||||
|
onError?: (message: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ScheduleService {
|
export default class ScheduleService {
|
||||||
private scheduleStacks = new Map<number, nodeSchedule.Job>();
|
private scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||||
|
|
||||||
|
private intervalSchedule = new ToadScheduler();
|
||||||
|
|
||||||
|
private maxBuffer = 200 * 1024 * 1024;
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
async generateSchedule({ id = 0, command, name, schedule }: Crontab) {
|
async runTask(command: string, callbacks: TaskCallbacks = {}) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const startTime = dayjs();
|
||||||
|
await callbacks.onBefore?.(startTime);
|
||||||
|
|
||||||
|
const cp = spawn(command, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
callbacks.onStart?.(cp, startTime);
|
||||||
|
|
||||||
|
cp.stdout.on('data', async (data) => {
|
||||||
|
await callbacks.onLog?.(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stderr.on('data', async (data) => {
|
||||||
|
this.logger.error(
|
||||||
|
'执行任务 %s 失败,时间:%s, 错误信息:%j',
|
||||||
|
command,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
data.toString(),
|
||||||
|
);
|
||||||
|
await callbacks.onError?.(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('error', async (err) => {
|
||||||
|
this.logger.error(
|
||||||
|
'创建任务 %s 失败,时间:%s, 错误信息:%j',
|
||||||
|
command,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
await callbacks.onError?.(JSON.stringify(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code, signal) => {
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'[创建定时任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||||
id,
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(null);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await this.logger.error(
|
||||||
|
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||||
|
command,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
await callbacks.onError?.(JSON.stringify(error));
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCronTask(
|
||||||
|
{ id = 0, command, name, schedule = '' }: ScheduleTaskType,
|
||||||
|
callbacks?: TaskCallbacks,
|
||||||
|
runImmediately = false,
|
||||||
|
) {
|
||||||
|
const _id = this.formatId(id);
|
||||||
|
this.logger.info(
|
||||||
|
'[创建cron任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
||||||
|
_id,
|
||||||
schedule,
|
schedule,
|
||||||
name,
|
name,
|
||||||
command,
|
command,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.scheduleStacks.set(
|
this.scheduleStacks.set(
|
||||||
id,
|
_id,
|
||||||
nodeSchedule.scheduleJob(id + '', schedule, async () => {
|
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||||
try {
|
await this.runTask(command, callbacks);
|
||||||
exec(command, async (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
await this.logger.info(
|
|
||||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
|
||||||
command,
|
|
||||||
new Date().toLocaleString(),
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
await this.logger.info(
|
|
||||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
|
||||||
command,
|
|
||||||
new Date().toLocaleString(),
|
|
||||||
stderr,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await this.logger.info(
|
|
||||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
|
||||||
command,
|
|
||||||
new Date().toLocaleString(),
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (runImmediately) {
|
||||||
|
await this.runTask(command, callbacks);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelSchedule({ id = 0, name }: Crontab) {
|
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
||||||
|
const _id = this.formatId(id);
|
||||||
this.logger.info('[取消定时任务],任务名:%s', name);
|
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||||
this.scheduleStacks.has(id) && this.scheduleStacks.get(id)?.cancel();
|
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createIntervalTask(
|
||||||
|
{ id = 0, command, name = '' }: ScheduleTaskType,
|
||||||
|
schedule: SimpleIntervalSchedule,
|
||||||
|
runImmediately = true,
|
||||||
|
callbacks?: TaskCallbacks,
|
||||||
|
) {
|
||||||
|
const _id = this.formatId(id);
|
||||||
|
this.logger.info(
|
||||||
|
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
|
||||||
|
_id,
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
const task = new AsyncTask(
|
||||||
|
name,
|
||||||
|
async () => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
await this.runTask(command, callbacks);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.logger.error(
|
||||||
|
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||||
|
command,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const job = new LongIntervalJob(
|
||||||
|
{ runImmediately: false, ...schedule },
|
||||||
|
task,
|
||||||
|
_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.intervalSchedule.addIntervalJob(job);
|
||||||
|
|
||||||
|
if (runImmediately) {
|
||||||
|
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.intervalSchedule.removeById(_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatId(id: number): string {
|
||||||
|
return String(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-26
@@ -1,8 +1,11 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { spawn } from 'child_process';
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import CronService from './cron';
|
import CronService from './cron';
|
||||||
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ScriptService {
|
export default class ScriptService {
|
||||||
@@ -10,44 +13,43 @@ export default class ScriptService {
|
|||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
private cronService: CronService,
|
private cronService: CronService,
|
||||||
|
private scheduleService: ScheduleService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async runScript(path: string) {
|
private taskCallbacks(filePath: string): TaskCallbacks {
|
||||||
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
|
return {
|
||||||
|
onEnd: async (cp, endTime, diff) => {
|
||||||
cp.stdout.on('data', (data) => {
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
onError: async (message: string) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'manuallyRunScript',
|
type: 'manuallyRunScript',
|
||||||
message: data.toString(),
|
message,
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
onLog: async (message: string) => {
|
||||||
cp.stderr.on('data', (data) => {
|
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'manuallyRunScript',
|
type: 'manuallyRunScript',
|
||||||
message: data.toString(),
|
message,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
cp.on('error', (err) => {
|
public async runScript(filePath: string) {
|
||||||
this.sockService.sendMessage({
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
type: 'manuallyRunScript',
|
const command = `task -l ${relativePath} now`;
|
||||||
message: JSON.stringify(err),
|
this.scheduleService.runTask(command, this.taskCallbacks(filePath));
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('close', (err) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'manuallyRunScript',
|
|
||||||
message: `执行结束`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stopScript(path: string) {
|
public async stopScript(filePath: string) {
|
||||||
const err = await this.cronService.killTask(`task -l ${path} now`);
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
|
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
|
||||||
|
|
||||||
const str = err ? `\n${err}` : '';
|
const str = err ? `\n${err}` : '';
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'manuallyRunScript',
|
type: 'manuallyRunScript',
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class SshKeyService {
|
||||||
|
private homedir = os.homedir();
|
||||||
|
private sshPath = path.resolve(this.homedir, '.ssh');
|
||||||
|
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
private generatePrivateKeyFile(alias: string, key: string): void {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
mode: '400',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('生成私钥文件失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getConfigRegx(alias: string) {
|
||||||
|
return new RegExp(
|
||||||
|
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
|
||||||
|
'g',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removePrivateKeyFile(alias: string): void {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(`${this.sshPath}/${alias}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('删除私钥文件失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSingleSshConfig(alias: string, host: string): string {
|
||||||
|
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSshConfig(configs: string[]) {
|
||||||
|
try {
|
||||||
|
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 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 removeSSHKey(alias: string, host: string): void {
|
||||||
|
this.removePrivateKeyFile(alias);
|
||||||
|
const config = this.generateSingleSshConfig(alias, host);
|
||||||
|
this.removeSshConfig(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import config from '../config';
|
||||||
|
import {
|
||||||
|
Subscription,
|
||||||
|
SubscriptionModel,
|
||||||
|
SubscriptionStatus,
|
||||||
|
} from '../data/subscription';
|
||||||
|
import {
|
||||||
|
ChildProcessWithoutNullStreams,
|
||||||
|
exec,
|
||||||
|
execSync,
|
||||||
|
spawn,
|
||||||
|
} from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import cron_parser from 'cron-parser';
|
||||||
|
import {
|
||||||
|
getFileContentByName,
|
||||||
|
concurrentRun,
|
||||||
|
fileExist,
|
||||||
|
createFile,
|
||||||
|
} from '../config/util';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class SubscriptionService {
|
||||||
|
constructor(
|
||||||
|
@Inject('logger') private logger: winston.Logger,
|
||||||
|
private scheduleService: ScheduleService,
|
||||||
|
private sockService: SockService,
|
||||||
|
private sshKeyService: SshKeyService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async list(searchText?: string): Promise<Subscription[]> {
|
||||||
|
let query = {};
|
||||||
|
if (searchText) {
|
||||||
|
const reg = {
|
||||||
|
[Op.or]: [
|
||||||
|
{ [Op.like]: `%${searchText}%` },
|
||||||
|
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
query = {
|
||||||
|
[Op.or]: [
|
||||||
|
{
|
||||||
|
name: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: reg,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await SubscriptionModel.findAll({
|
||||||
|
where: query,
|
||||||
|
order: [['createdAt', 'DESC']],
|
||||||
|
});
|
||||||
|
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, 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 = this.formatCommand(doc, url as string);
|
||||||
|
|
||||||
|
if (doc.schedule_type === 'crontab') {
|
||||||
|
this.scheduleService.cancelCronTask(doc as any);
|
||||||
|
needCreate &&
|
||||||
|
(await this.scheduleService.createCronTask(
|
||||||
|
doc as any,
|
||||||
|
this.taskCallbacks(doc),
|
||||||
|
runImmediately,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
this.scheduleService.cancelIntervalTask(doc as any);
|
||||||
|
const { type, value } = doc.interval_schedule as any;
|
||||||
|
needCreate &&
|
||||||
|
(await this.scheduleService.createIntervalTask(
|
||||||
|
doc as any,
|
||||||
|
{ [type]: value } as SimpleIntervalSchedule,
|
||||||
|
runImmediately,
|
||||||
|
this.taskCallbacks(doc),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return {
|
||||||
|
onBefore: async (startTime) => {
|
||||||
|
const logTime = startTime.format('YYYY-MM-DD-HH-mm-ss');
|
||||||
|
const logPath = `${doc.alias}/${logTime}.log`;
|
||||||
|
await SubscriptionModel.update(
|
||||||
|
{
|
||||||
|
status: SubscriptionStatus.running,
|
||||||
|
log_path: logPath,
|
||||||
|
},
|
||||||
|
{ where: { id: doc.id } },
|
||||||
|
);
|
||||||
|
const absolutePath = await this.handleLogPath(
|
||||||
|
logPath as string,
|
||||||
|
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 执行sub_before
|
||||||
|
let beforeStr = '';
|
||||||
|
try {
|
||||||
|
if (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) {
|
||||||
|
fs.appendFileSync(absolutePath, `${beforeStr}\n`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
|
||||||
|
await SubscriptionModel.update(
|
||||||
|
{
|
||||||
|
pid: cp.pid,
|
||||||
|
},
|
||||||
|
{ where: { id: doc.id } },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onEnd: async (cp, endTime, diff) => {
|
||||||
|
const sub = await this.getDb({ id: doc.id });
|
||||||
|
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
||||||
|
|
||||||
|
// 执行 sub_after
|
||||||
|
let afterStr = '';
|
||||||
|
try {
|
||||||
|
if (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) {
|
||||||
|
fs.appendFileSync(absolutePath, `${afterStr}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.appendFileSync(
|
||||||
|
absolutePath,
|
||||||
|
`\n## 执行结束... ${endTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)} 耗时 ${diff} 秒`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await SubscriptionModel.update(
|
||||||
|
{ status: SubscriptionStatus.idle, pid: undefined },
|
||||||
|
{ where: { id: sub.id } },
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'runSubscriptionEnd',
|
||||||
|
message: '订阅执行完成',
|
||||||
|
references: [doc.id as number],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: async (message: string) => {
|
||||||
|
const sub = await this.getDb({ id: doc.id });
|
||||||
|
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 this.handleLogPath(sub.log_path as string);
|
||||||
|
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(payload: Subscription): Promise<Subscription> {
|
||||||
|
const tab = new Subscription(payload);
|
||||||
|
const doc = await this.insert(tab);
|
||||||
|
await this.handleTask(doc);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insert(payload: Subscription): Promise<Subscription> {
|
||||||
|
return await SubscriptionModel.create(payload, { returning: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(payload: Subscription): Promise<Subscription> {
|
||||||
|
const newDoc = await this.updateDb(payload);
|
||||||
|
await this.handleTask(newDoc, !newDoc.is_disabled);
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateDb(payload: Subscription): Promise<Subscription> {
|
||||||
|
await SubscriptionModel.update(payload, { where: { id: payload.id } });
|
||||||
|
return await this.getDb({ id: payload.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async status({
|
||||||
|
ids,
|
||||||
|
status,
|
||||||
|
pid,
|
||||||
|
log_path,
|
||||||
|
last_running_time = 0,
|
||||||
|
last_execution_time = 0,
|
||||||
|
}: {
|
||||||
|
ids: number[];
|
||||||
|
status: SubscriptionStatus;
|
||||||
|
pid: number;
|
||||||
|
log_path: string;
|
||||||
|
last_running_time: number;
|
||||||
|
last_execution_time: number;
|
||||||
|
}) {
|
||||||
|
const options: any = {
|
||||||
|
status,
|
||||||
|
pid,
|
||||||
|
log_path,
|
||||||
|
last_execution_time,
|
||||||
|
};
|
||||||
|
if (last_running_time > 0) {
|
||||||
|
options.last_running_time = last_running_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await SubscriptionModel.update(
|
||||||
|
{ ...options },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(ids: number[]) {
|
||||||
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
await this.handleTask(doc, false, false);
|
||||||
|
}
|
||||||
|
await SubscriptionModel.destroy({ where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
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[]) {
|
||||||
|
await SubscriptionModel.update(
|
||||||
|
{ status: SubscriptionStatus.queued },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
concurrentRun(
|
||||||
|
ids.map((id) => async () => await this.runSingle(id)),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(ids: number[]) {
|
||||||
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc.pid) {
|
||||||
|
try {
|
||||||
|
process.kill(-doc.pid);
|
||||||
|
} catch (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(
|
||||||
|
{ status: SubscriptionStatus.idle, pid: undefined },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = this.formatCommand(subscription);
|
||||||
|
|
||||||
|
await this.scheduleService.runTask(
|
||||||
|
command,
|
||||||
|
this.taskCallbacks(subscription),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disabled(ids: number[]) {
|
||||||
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
await this.handleTask(doc, false);
|
||||||
|
}
|
||||||
|
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enabled(ids: number[]) {
|
||||||
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
await this.handleTask(doc);
|
||||||
|
}
|
||||||
|
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async log(id: number) {
|
||||||
|
const doc = await this.getDb({ id });
|
||||||
|
if (!doc || !doc.log_path) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
||||||
|
return getFileContentByName(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logs(id: number) {
|
||||||
|
const doc = await this.getDb({ id });
|
||||||
|
if (!doc) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.log_path) {
|
||||||
|
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||||
|
const dir = path.resolve(config.logPath, relativeDir);
|
||||||
|
if (existsSync(dir)) {
|
||||||
|
let files = await promises.readdir(dir);
|
||||||
|
return files
|
||||||
|
.map((x) => ({
|
||||||
|
filename: x,
|
||||||
|
directory: relativeDir.replace(config.logPath, ''),
|
||||||
|
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.time - a.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-11
@@ -24,7 +24,7 @@ export default class SystemService {
|
|||||||
|
|
||||||
public async getLogRemoveFrequency() {
|
public async getLogRemoveFrequency() {
|
||||||
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
|
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
|
||||||
return (doc && doc.info) || {};
|
return doc || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||||
@@ -52,12 +52,14 @@ export default class SystemService {
|
|||||||
});
|
});
|
||||||
return { code: 200, data: { ...result, code } };
|
return { code: 200, data: { ...result, code } };
|
||||||
} else {
|
} else {
|
||||||
return { code: 400, data: '通知发送失败,请检查参数' };
|
return { code: 400, message: '通知发送失败,请检查参数' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateLogRemoveFrequency(frequency: number) {
|
public async updateLogRemoveFrequency(frequency: number) {
|
||||||
|
const oDoc = await this.getLogRemoveFrequency();
|
||||||
const result = await this.updateAuthDb({
|
const result = await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
type: AuthDataType.removeLogFrequency,
|
type: AuthDataType.removeLogFrequency,
|
||||||
info: { frequency },
|
info: { frequency },
|
||||||
});
|
});
|
||||||
@@ -65,11 +67,13 @@ export default class SystemService {
|
|||||||
id: result.id,
|
id: result.id,
|
||||||
name: '删除日志',
|
name: '删除日志',
|
||||||
command: `ql rmlog ${frequency}`,
|
command: `ql rmlog ${frequency}`,
|
||||||
schedule: `5 23 */${frequency} * *`,
|
|
||||||
};
|
};
|
||||||
await this.scheduleService.cancelSchedule(cron);
|
await this.scheduleService.cancelIntervalTask(cron);
|
||||||
if (frequency > 0) {
|
if (frequency > 0) {
|
||||||
await this.scheduleService.generateSchedule(cron);
|
await this.scheduleService.createIntervalTask(cron, {
|
||||||
|
days: frequency,
|
||||||
|
runImmediately: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return { code: 200, data: { ...cron } };
|
return { code: 200, data: { ...cron } };
|
||||||
}
|
}
|
||||||
@@ -85,13 +89,10 @@ export default class SystemService {
|
|||||||
let lastVersion = '';
|
let lastVersion = '';
|
||||||
let lastLog = '';
|
let lastLog = '';
|
||||||
try {
|
try {
|
||||||
const result = await Promise.race([
|
const result = await got.get(config.lastVersionFile, {
|
||||||
got.get(config.lastVersionFile, { timeout: 6000, retry: 0 }),
|
|
||||||
got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
|
|
||||||
timeout: 6000,
|
timeout: 6000,
|
||||||
retry: 0,
|
retry: 0,
|
||||||
}),
|
});
|
||||||
]);
|
|
||||||
const lastVersionFileContent = result.body;
|
const lastVersionFileContent = result.body;
|
||||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||||
lastLog = lastVersionFileContent.match(logRegx)
|
lastLog = lastVersionFileContent.match(logRegx)
|
||||||
@@ -110,7 +111,7 @@ export default class SystemService {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
code: 400,
|
code: 400,
|
||||||
data: error.message,
|
message: error.message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,4 +165,13 @@ export default class SystemService {
|
|||||||
|
|
||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async notify({ title, content }: { title: string; content: string }) {
|
||||||
|
const isSuccess = await this.notificationService.notify(title, content);
|
||||||
|
if (isSuccess) {
|
||||||
|
return { code: 200, message: '通知发送成功' };
|
||||||
|
} else {
|
||||||
|
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-118
@@ -13,7 +13,7 @@ import { Request } from 'express';
|
|||||||
import ScheduleService from './schedule';
|
import ScheduleService from './schedule';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import got from 'got';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class UserService {
|
export default class UserService {
|
||||||
@@ -111,7 +111,7 @@ export default class UserService {
|
|||||||
});
|
});
|
||||||
await this.notificationService.notify(
|
await this.notificationService.notify(
|
||||||
'登录通知',
|
'登录通知',
|
||||||
`你于${new Date(timestamp).toLocaleString()}在 ${address} ${
|
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||||
req.platform
|
req.platform
|
||||||
}端 登录成功,ip地址 ${ip}`,
|
}端 登录成功,ip地址 ${ip}`,
|
||||||
);
|
);
|
||||||
@@ -140,7 +140,7 @@ export default class UserService {
|
|||||||
});
|
});
|
||||||
await this.notificationService.notify(
|
await this.notificationService.notify(
|
||||||
'登录通知',
|
'登录通知',
|
||||||
`你于${new Date(timestamp).toLocaleString()}在 ${address} ${
|
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||||
req.platform
|
req.platform
|
||||||
}端 登录失败,ip地址 ${ip}`,
|
}端 登录失败,ip地址 ${ip}`,
|
||||||
);
|
);
|
||||||
@@ -221,6 +221,12 @@ export default class UserService {
|
|||||||
return { code: 200, message: '更新成功' };
|
return { code: 200, message: '更新成功' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateAvatar(avatar: string) {
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
this.updateAuthInfo(authInfo, { avatar });
|
||||||
|
return { code: 200, data: avatar, message: '更新成功' };
|
||||||
|
}
|
||||||
|
|
||||||
public getUserInfo(): Promise<any> {
|
public getUserInfo(): Promise<any> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
||||||
@@ -307,11 +313,6 @@ export default class UserService {
|
|||||||
return (doc && doc.info) || {};
|
return (doc && doc.info) || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLogRemoveFrequency() {
|
|
||||||
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
|
|
||||||
return (doc && doc.info) || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||||
let doc = await AuthModel.findOne({ type: payload.type });
|
let doc = await AuthModel.findOne({ type: payload.type });
|
||||||
if (doc) {
|
if (doc) {
|
||||||
@@ -345,116 +346,7 @@ export default class UserService {
|
|||||||
});
|
});
|
||||||
return { code: 200, data: { ...result, code } };
|
return { code: 200, data: { ...result, code } };
|
||||||
} else {
|
} else {
|
||||||
return { code: 400, data: '通知发送失败,请检查参数' };
|
return { code: 400, message: '通知发送失败,请检查参数' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateLogRemoveFrequency(frequency: number) {
|
|
||||||
const result = await this.updateAuthDb({
|
|
||||||
type: AuthDataType.removeLogFrequency,
|
|
||||||
info: { frequency },
|
|
||||||
});
|
|
||||||
const cron = {
|
|
||||||
id: result.id,
|
|
||||||
name: '删除日志',
|
|
||||||
command: `ql rmlog ${frequency}`,
|
|
||||||
schedule: `5 23 */${frequency} * *`,
|
|
||||||
};
|
|
||||||
await this.scheduleService.cancelSchedule(cron);
|
|
||||||
if (frequency > 0) {
|
|
||||||
await this.scheduleService.generateSchedule(cron);
|
|
||||||
}
|
|
||||||
return { code: 200, data: { ...cron } };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkUpdate() {
|
|
||||||
try {
|
|
||||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
|
||||||
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
|
||||||
|
|
||||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
|
||||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
|
||||||
|
|
||||||
let lastVersion = '';
|
|
||||||
let lastLog = '';
|
|
||||||
try {
|
|
||||||
const result = await Promise.race([
|
|
||||||
got.get(config.lastVersionFile, { timeout: 1000, retry: 0 }),
|
|
||||||
got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
|
|
||||||
timeout: 5000,
|
|
||||||
retry: 0,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
const lastVersionFileContent = result.body;
|
|
||||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
|
||||||
lastLog = lastVersionFileContent.match(logRegx)
|
|
||||||
? lastVersionFileContent.match(logRegx)![1]
|
|
||||||
: '';
|
|
||||||
} catch (error) {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
data: {
|
|
||||||
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
|
||||||
lastVersion,
|
|
||||||
lastLog,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
return {
|
|
||||||
code: 400,
|
|
||||||
data: error.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkHasNewVersion(curVersion: string, lastVersion: string) {
|
|
||||||
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
|
|
||||||
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
|
|
||||||
if (curArr[0] < lastArr[0]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
curArr[0] === lastArr[0] &&
|
|
||||||
curArr[1] === lastArr[1] &&
|
|
||||||
curArr[2] < lastArr[2]
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateSystem() {
|
|
||||||
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',
|
|
||||||
message: data.toString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.stderr.on('data', (data) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'updateSystemVersion',
|
|
||||||
message: data.toString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('error', (err) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'updateSystemVersion',
|
|
||||||
message: JSON.stringify(err),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return { code: 200 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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), { encoding: 'utf8' }, () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken();
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
image: whyour/qinglong:latest
|
|
||||||
volumes:
|
|
||||||
- ./data/config:/ql/config
|
|
||||||
- ./data/log:/ql/log
|
|
||||||
- ./data/db:/ql/db
|
|
||||||
- ./data/scripts:/ql/scripts
|
|
||||||
- ./data/repo:/ql/repo
|
|
||||||
- ./data/raw:/ql/raw
|
|
||||||
ports:
|
|
||||||
- "0.0.0.0:5700:5700"
|
|
||||||
restart: unless-stopped
|
|
||||||
+37
-6
@@ -1,10 +1,12 @@
|
|||||||
FROM whyour/ql:base
|
FROM node:alpine
|
||||||
|
|
||||||
ARG QL_MAINTAINER="whyour"
|
ARG QL_MAINTAINER="whyour"
|
||||||
LABEL maintainer="${QL_MAINTAINER}"
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||||
ARG QL_BRANCH=master
|
ARG QL_BRANCH=master
|
||||||
|
|
||||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
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 \
|
||||||
LANG=zh_CN.UTF-8 \
|
LANG=zh_CN.UTF-8 \
|
||||||
SHELL=/bin/bash \
|
SHELL=/bin/bash \
|
||||||
PS1="\u@\h:\w \$ " \
|
PS1="\u@\h:\w \$ " \
|
||||||
@@ -13,17 +15,46 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
|||||||
|
|
||||||
WORKDIR ${QL_DIR}
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
RUN git clone -b ${QL_BRANCH} ${QL_URL} ${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 \
|
||||||
|
tzdata \
|
||||||
|
perl \
|
||||||
|
openssl \
|
||||||
|
nginx \
|
||||||
|
python3 \
|
||||||
|
jq \
|
||||||
|
openssh \
|
||||||
|
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 \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& pnpm add -g pm2 ts-node typescript tslib \
|
||||||
|
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||||
&& cd ${QL_DIR} \
|
&& cd ${QL_DIR} \
|
||||||
&& cp -f .env.example .env \
|
&& cp -f .env.example .env \
|
||||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||||
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||||
&& cp -rf /node_modules ./ \
|
|
||||||
&& rm -rf /node_modules \
|
|
||||||
&& pnpm install --prod \
|
&& pnpm install --prod \
|
||||||
&& rm -rf /root/.pnpm-store \
|
&& rm -rf /root/.pnpm-store \
|
||||||
|
&& rm -rf /root/.local/share/pnpm/store \
|
||||||
&& rm -rf /root/.cache \
|
&& rm -rf /root/.cache \
|
||||||
|
&& rm -rf /root/.npm \
|
||||||
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||||
&& cp -rf /static/* ${QL_DIR} \
|
&& mkdir -p ${QL_DIR}/static \
|
||||||
|
&& cp -rf /static/* ${QL_DIR}/static \
|
||||||
&& rm -rf /static
|
&& rm -rf /static
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
FROM node:alpine
|
|
||||||
ARG QL_MAINTAINER="whyour"
|
|
||||||
LABEL maintainer="${QL_MAINTAINER}"
|
|
||||||
|
|
||||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
|
||||||
LANG=zh_CN.UTF-8 \
|
|
||||||
SHELL=/bin/bash \
|
|
||||||
PS1="\u@\h:\w \$ "
|
|
||||||
|
|
||||||
COPY package.json /
|
|
||||||
|
|
||||||
RUN set -x \
|
|
||||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
|
|
||||||
&& apk update -f \
|
|
||||||
&& apk upgrade \
|
|
||||||
&& apk --no-cache add -f bash \
|
|
||||||
coreutils \
|
|
||||||
moreutils \
|
|
||||||
git \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
tzdata \
|
|
||||||
perl \
|
|
||||||
openssl \
|
|
||||||
nginx \
|
|
||||||
python3 \
|
|
||||||
jq \
|
|
||||||
openssh \
|
|
||||||
py3-pip \
|
|
||||||
python2 \
|
|
||||||
g++ \
|
|
||||||
make \
|
|
||||||
&& rm -rf /var/cache/apk/* \
|
|
||||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
|
||||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
|
||||||
&& touch ~/.bashrc \
|
|
||||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
|
||||||
&& git config --global user.name "qinglong" \
|
|
||||||
&& npm install -g pnpm \
|
|
||||||
&& pnpm install -g pm2 \
|
|
||||||
&& pnpm install -g ts-node typescript tslib \
|
|
||||||
&& cd / && pnpm install --prod \
|
|
||||||
&& apk --purge del python2 g++ make \
|
|
||||||
&& rm -rf /root/.npm \
|
|
||||||
&& rm -rf /root/.pnpm-store \
|
|
||||||
&& rm -rf /root/.cache \
|
|
||||||
&& rm -f /package.json
|
|
||||||
|
|
||||||
CMD [ "node" ]
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: whyour/qinglong:latest
|
||||||
|
volumes:
|
||||||
|
- ./data:/ql/data
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:5700:5700"
|
||||||
|
restart: unless-stopped
|
||||||
@@ -20,24 +20,29 @@ echo -e "======================3. 启动nginx========================\n"
|
|||||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||||
echo -e "nginx启动成功...\n"
|
echo -e "nginx启动成功...\n"
|
||||||
|
|
||||||
echo -e "======================4. 启动控制面板========================\n"
|
echo -e "======================4. 启动面板监控========================\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 delete panel &>/dev/null
|
||||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time
|
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
|
||||||
echo -e "控制面板启动成功...\n"
|
echo -e "控制面板启动成功...\n"
|
||||||
|
|
||||||
echo -e "======================5. 启动定时任务========================\n"
|
echo -e "======================6. 启动定时任务========================\n"
|
||||||
pm2 delete schedule &>/dev/null
|
pm2 delete schedule &>/dev/null
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
|
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
|
||||||
echo -e "定时任务启动成功...\n"
|
echo -e "定时任务启动成功...\n"
|
||||||
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
echo -e "======================6. 启动bot========================\n"
|
echo -e "======================7. 启动bot========================\n"
|
||||||
nohup ql bot >>$dir_log/start.log 2>&1 &
|
nohup ql bot >>$dir_log/start.log 2>&1 &
|
||||||
echo -e "bot后台启动中...\n"
|
echo -e "bot后台启动中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EnableExtraShell == true ]]; then
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
echo -e "======================7. 执行自定义脚本========================\n"
|
echo -e "======================8. 执行自定义脚本========================\n"
|
||||||
nohup ql extra >>$dir_log/start.log 2>&1 &
|
nohup ql extra >>$dir_log/start.log 2>&1 &
|
||||||
echo -e "自定义脚本后台执行中...\n"
|
echo -e "自定义脚本后台执行中...\n"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+12
-1
@@ -2,6 +2,10 @@ upstream api {
|
|||||||
server 0.0.0.0:5600;
|
server 0.0.0.0:5600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstream public {
|
||||||
|
server 0.0.0.0:5400;
|
||||||
|
}
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
map $http_upgrade $connection_upgrade {
|
||||||
default keep-alive;
|
default keep-alive;
|
||||||
'websocket' upgrade;
|
'websocket' upgrade;
|
||||||
@@ -9,9 +13,16 @@ map $http_upgrade $connection_upgrade {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 5700;
|
listen 5700;
|
||||||
root /ql/dist;
|
root /ql/static/dist;
|
||||||
ssl_session_timeout 5m;
|
ssl_session_timeout 5m;
|
||||||
|
|
||||||
|
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://public;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|||||||
+70
-53
@@ -4,12 +4,14 @@
|
|||||||
"start": "concurrently -n w: npm:start:*",
|
"start": "concurrently -n w: npm:start:*",
|
||||||
"start:front": "umi dev",
|
"start:front": "umi dev",
|
||||||
"start:back": "nodemon",
|
"start:back": "nodemon",
|
||||||
|
"start:public": "ts-node back/public.ts",
|
||||||
"build:front": "umi build",
|
"build:front": "umi build",
|
||||||
"build:back": "tsc -p tsconfig.back.json",
|
"build:back": "tsc -p tsconfig.back.json",
|
||||||
"panel": "npm run build:back && node build/app.js",
|
"panel": "npm run build:back && node static/build/app.js",
|
||||||
"schedule": "npm run build:back && node build/schedule.js",
|
"schedule": "npm run build:back && node static/build/schedule.js",
|
||||||
|
"public": "npm run build:back && node static/build/public.js",
|
||||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||||
"prepare": "umi generate tmp",
|
"postinstall": "umi generate tmp 2>/dev/null || true",
|
||||||
"test": "umi-test",
|
"test": "umi-test",
|
||||||
"test:coverage": "umi-test --coverage"
|
"test:coverage": "umi-test --coverage"
|
||||||
},
|
},
|
||||||
@@ -24,73 +26,88 @@
|
|||||||
"prettier --parser=typescript --write"
|
"prettier --parser=typescript --write"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"peerDependencyRules": {
|
||||||
|
"allowedVersions": {
|
||||||
|
"react": "17",
|
||||||
|
"react-dom": "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@otplib/preset-default": "^12.0.1",
|
"@otplib/preset-default": "^12.0.1",
|
||||||
"@sentry/node": "^6.17.2",
|
"@sentry/node": "^7.0.0",
|
||||||
"@sentry/tracing": "^6.17.2",
|
"@sentry/tracing": "^7.0.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.2",
|
||||||
"celebrate": "^13.0.3",
|
"celebrate": "^15.0.1",
|
||||||
"chokidar": "^3.5.2",
|
"chokidar": "^3.5.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron-parser": "^3.5.0",
|
"cron-parser": "^4.2.1",
|
||||||
"dotenv": "^8.2.0",
|
"dayjs": "^1.11.2",
|
||||||
"esbuild": "^0.14.14",
|
"dotenv": "^16.0.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.3",
|
||||||
"express-jwt": "^6.0.0",
|
"express-jwt": "^6.1.1",
|
||||||
"express-urlrewrite": "^1.4.0",
|
"express-urlrewrite": "^1.4.0",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
"hpagent": "^0.1.2",
|
"hpagent": "^0.1.2",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"multer": "^1.4.4",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-schedule": "^2.1.0",
|
||||||
"node-schedule": "^2.0.0",
|
"nodemailer": "^6.7.2",
|
||||||
"nodemailer": "^6.7.0",
|
"p-queue": "7.2.0",
|
||||||
"p-queue": "6.6.2",
|
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sequelize": "^7.0.0-alpha.3",
|
"sequelize": "^6.20.1",
|
||||||
"serve-handler": "^6.1.3",
|
"serve-handler": "^6.1.3",
|
||||||
"sockjs": "^0.3.21",
|
"sockjs": "^0.3.24",
|
||||||
"sqlite3": "^5.0.2",
|
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
|
||||||
"typedi": "^0.8.0",
|
"toad-scheduler": "^1.6.0",
|
||||||
|
"typedi": "^0.10.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"winston": "^3.3.3",
|
"winston": "^3.6.0",
|
||||||
"yargs": "^17.2.1"
|
"yargs": "^17.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^4.6.2",
|
"@ant-design/icons": "^4.7.0",
|
||||||
"@ant-design/pro-layout": "^6.26.0",
|
"@ant-design/pro-layout": "^6.33.1",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@monaco-editor/react": "^4.3.1",
|
||||||
"@sentry/react": "^6.17.2",
|
"@sentry/react": "^7.0.0",
|
||||||
"@types/cors": "^2.8.10",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/express": "^4.17.8",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/express-jwt": "^6.0.1",
|
"@types/express": "^4.17.13",
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/express-jwt": "^6.0.4",
|
||||||
"@types/lodash": "^4.14.170",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/nedb": "^1.8.11",
|
"@types/lodash": "^4.14.179",
|
||||||
"@types/node": "^14.11.2",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node-fetch": "^2.5.8",
|
"@types/nedb": "^1.8.12",
|
||||||
|
"@types/node": "^17.0.21",
|
||||||
"@types/node-schedule": "^1.3.2",
|
"@types/node-schedule": "^1.3.2",
|
||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/qrcode.react": "^1.0.1",
|
"@types/qrcode.react": "^1.0.2",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.39",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.13",
|
||||||
"@types/serve-handler": "^6.1.1",
|
"@types/serve-handler": "^6.1.1",
|
||||||
"@types/sockjs": "^0.3.33",
|
"@types/sockjs": "^0.3.33",
|
||||||
"@types/sockjs-client": "^1.5.1",
|
"@types/sockjs-client": "^1.5.1",
|
||||||
"@types/uuid": "^8.3.3",
|
"@types/uuid": "^8.3.4",
|
||||||
"@umijs/plugin-antd": "^0.11.0",
|
"@umijs/plugin-antd": "^0.15.0",
|
||||||
"@umijs/test": "^3.3.9",
|
"@umijs/plugin-esbuild": "^1.4.1",
|
||||||
"antd": "^4.17.0-alpha.6",
|
"@umijs/ssr-darkreader": "^4.9.45",
|
||||||
"codemirror": "^5.62.2",
|
"@umijs/test": "^3.5.21",
|
||||||
"compression-webpack-plugin": "6.1.1",
|
"ansi-to-react": "^6.1.6",
|
||||||
|
"antd": "^4.20.5",
|
||||||
|
"antd-img-crop": "^4.2.3",
|
||||||
|
"codemirror": "^5.65.2",
|
||||||
|
"compression-webpack-plugin": "9.2.0",
|
||||||
"concurrently": "^7.0.0",
|
"concurrently": "^7.0.0",
|
||||||
"darkreader": "4.9.40",
|
"lint-staged": "^12.3.4",
|
||||||
"lint-staged": "^10.0.7",
|
"nodemon": "^2.0.15",
|
||||||
"nodemon": "^2.0.4",
|
"prettier": "^2.5.1",
|
||||||
"prettier": "^2.2.0",
|
"qiniu": "^7.4.0",
|
||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
|
"rc-tween-one": "^3.0.6",
|
||||||
"react": "17.x",
|
"react": "17.x",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react-codemirror2": "^7.2.1",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
@@ -98,13 +115,13 @@
|
|||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^14.0.0",
|
||||||
"react-dom": "17.x",
|
"react-dom": "17.x",
|
||||||
"react-split-pane": "^0.1.92",
|
"react-split-pane": "^0.1.92",
|
||||||
"sockjs-client": "^1.5.2",
|
"sockjs-client": "^1.6.0",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^10.6.0",
|
||||||
"typescript": "^4.1.2",
|
"typescript": "^4.6.2",
|
||||||
"umi": "^3.5.0",
|
"umi": "^3.5.21",
|
||||||
"umi-request": "^1.3.5",
|
"umi-request": "^1.4.0",
|
||||||
"vh-check": "^2.0.5",
|
"vh-check": "^2.0.5",
|
||||||
"webpack": "^5.28.0",
|
"webpack": "^5.70.0",
|
||||||
"yorkie": "^2.0.0"
|
"yorkie": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ AutoStartBot=""
|
|||||||
BotRepoUrl=""
|
BotRepoUrl=""
|
||||||
|
|
||||||
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
||||||
PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
|
PipMirror="https://pypi.doubanio.com/simple/"
|
||||||
|
|
||||||
## 通知环境变量
|
## 通知环境变量
|
||||||
## 1. Server酱
|
## 1. Server酱
|
||||||
@@ -50,6 +50,8 @@ export PUSH_KEY=""
|
|||||||
## 2. BARK
|
## 2. BARK
|
||||||
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
|
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
|
||||||
export BARK_PUSH=""
|
export BARK_PUSH=""
|
||||||
|
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
|
||||||
|
export BARK_ICON="http://qn.whyour.cn/logo.png"
|
||||||
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
|
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
|
||||||
export BARK_SOUND=""
|
export BARK_SOUND=""
|
||||||
## 下方填写推送消息分组,默认为"QingLong"
|
## 下方填写推送消息分组,默认为"QingLong"
|
||||||
@@ -123,4 +125,8 @@ export GOTIFY_URL=""
|
|||||||
export GOTIFY_TOKEN=""
|
export GOTIFY_TOKEN=""
|
||||||
export GOTIFY_PRIORITY=0
|
export GOTIFY_PRIORITY=0
|
||||||
|
|
||||||
|
## 11. PushDeer
|
||||||
|
## deer_key 填写PushDeer的key
|
||||||
|
export DEER_KEY=""
|
||||||
|
|
||||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 添加你需要重启自动执行的任意命令,比如 ql repo
|
## 添加你需要重启自动执行的任意命令,比如 ql repo
|
||||||
## 安装node依赖使用 pnpm install -g xxx xxx
|
## 安装node依赖使用 pnpm add -g xxx xxx
|
||||||
## 安装python依赖使用 pip3 install xxx
|
## 安装python依赖使用 pip3 install xxx
|
||||||
|
|||||||
+83
-8
@@ -34,9 +34,16 @@ let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 us
|
|||||||
//(环境变量名 PUSH_KEY)
|
//(环境变量名 PUSH_KEY)
|
||||||
let SCKEY = '';
|
let SCKEY = '';
|
||||||
|
|
||||||
|
// =======================================PushDeer通知设置区域===========================================
|
||||||
|
//此处填你申请的PushDeer KEY.
|
||||||
|
//(环境变量名 DEER_KEY)
|
||||||
|
let PUSHDEER_KEY = '';
|
||||||
|
|
||||||
// =======================================Bark App通知设置区域===========================================
|
// =======================================Bark App通知设置区域===========================================
|
||||||
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
|
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
|
||||||
let BARK_PUSH = '';
|
let BARK_PUSH = '';
|
||||||
|
//BARK app推送图标,自定义推送图标(需iOS15或以上)
|
||||||
|
let BARK_ICON = 'http://qn.whyour.cn/logo.png';
|
||||||
//BARK app推送铃声,铃声列表去APP查看复制填写
|
//BARK app推送铃声,铃声列表去APP查看复制填写
|
||||||
let BARK_SOUND = '';
|
let BARK_SOUND = '';
|
||||||
//BARK app推送消息的分组, 默认为"QingLong"
|
//BARK app推送消息的分组, 默认为"QingLong"
|
||||||
@@ -90,6 +97,13 @@ let IGOT_PUSH_KEY = '';
|
|||||||
let PUSH_PLUS_TOKEN = '';
|
let PUSH_PLUS_TOKEN = '';
|
||||||
let PUSH_PLUS_USER = '';
|
let PUSH_PLUS_USER = '';
|
||||||
|
|
||||||
|
// =======================================Cool Push设置区域=======================================
|
||||||
|
//官方文档:https://cp.xuthus.cc/docs
|
||||||
|
//QQ_SKEY: Cool Push登录授权后推送消息的调用代码Skey
|
||||||
|
//QQ_MODE: 推送模式详情请登录获取QQ_SKEY后见https://cp.xuthus.cc/feat
|
||||||
|
let QQ_SKEY = '';
|
||||||
|
let QQ_MODE = '';
|
||||||
|
|
||||||
//==========================云端环境变量的判断与接收=========================
|
//==========================云端环境变量的判断与接收=========================
|
||||||
if (process.env.GOTIFY_URL) {
|
if (process.env.GOTIFY_URL) {
|
||||||
GOTIFY_URL = process.env.GOTIFY_URL;
|
GOTIFY_URL = process.env.GOTIFY_URL;
|
||||||
@@ -115,6 +129,10 @@ if (process.env.PUSH_KEY) {
|
|||||||
SCKEY = process.env.PUSH_KEY;
|
SCKEY = process.env.PUSH_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.DEER_KEY) {
|
||||||
|
PUSHDEER_KEY = process.env.DEER_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.QQ_SKEY) {
|
if (process.env.QQ_SKEY) {
|
||||||
QQ_SKEY = process.env.QQ_SKEY;
|
QQ_SKEY = process.env.QQ_SKEY;
|
||||||
}
|
}
|
||||||
@@ -133,6 +151,9 @@ if (process.env.BARK_PUSH) {
|
|||||||
} else {
|
} else {
|
||||||
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
|
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
|
||||||
}
|
}
|
||||||
|
if (process.env.BARK_ICON) {
|
||||||
|
BARK_ICON = process.env.BARK_ICON;
|
||||||
|
}
|
||||||
if (process.env.BARK_SOUND) {
|
if (process.env.BARK_SOUND) {
|
||||||
BARK_SOUND = process.env.BARK_SOUND;
|
BARK_SOUND = process.env.BARK_SOUND;
|
||||||
}
|
}
|
||||||
@@ -226,10 +247,12 @@ function gotifyNotify(text, desp) {
|
|||||||
if (GOTIFY_URL && GOTIFY_TOKEN) {
|
if (GOTIFY_URL && GOTIFY_TOKEN) {
|
||||||
const options = {
|
const options = {
|
||||||
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
|
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
|
||||||
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(desp)}&priority=${GOTIFY_PRIORITY}`,
|
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(
|
||||||
|
desp,
|
||||||
|
)}&priority=${GOTIFY_PRIORITY}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
$.post(options, (err, resp, data) => {
|
$.post(options, (err, resp, data) => {
|
||||||
try {
|
try {
|
||||||
@@ -346,6 +369,52 @@ function serverNotify(text, desp, time = 2100) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PushDeerNotify(text, desp, time = 2100) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (PUSHDEER_KEY) {
|
||||||
|
// PushDeer 建议对消息内容进行 urlencode
|
||||||
|
desp = encodeURI(desp);
|
||||||
|
const options = {
|
||||||
|
url: `https://api2.pushdeer.com/message/push`,
|
||||||
|
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('发送通知调用API失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
// 通过返回的result的长度来判断是否成功
|
||||||
|
if (
|
||||||
|
data.content.result.length !== undefined &&
|
||||||
|
data.content.result.length > 0
|
||||||
|
) {
|
||||||
|
console.log('PushDeer发送通知消息成功🎉\n');
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, time);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function CoolPush(text, desp) {
|
function CoolPush(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (QQ_SKEY) {
|
if (QQ_SKEY) {
|
||||||
@@ -429,7 +498,9 @@ function BarkNotify(text, desp, params = {}) {
|
|||||||
const options = {
|
const options = {
|
||||||
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
|
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
|
||||||
desp,
|
desp,
|
||||||
)}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(params)}`,
|
)}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
|
||||||
|
params,
|
||||||
|
)}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
@@ -465,9 +536,13 @@ function tgBotNotify(text, desp) {
|
|||||||
if (TG_BOT_TOKEN && TG_USER_ID) {
|
if (TG_BOT_TOKEN && TG_USER_ID) {
|
||||||
const options = {
|
const options = {
|
||||||
url: `https://${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
|
url: `https://${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
|
||||||
body: `chat_id=${TG_USER_ID}&text=${text}\n\n${desp}&disable_web_page_preview=true`,
|
json: {
|
||||||
|
chat_id: `${TG_USER_ID}`,
|
||||||
|
text: `${text}\n\n${desp}`,
|
||||||
|
disable_web_page_preview: true,
|
||||||
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
timeout,
|
timeout,
|
||||||
};
|
};
|
||||||
@@ -655,9 +730,9 @@ function qywxamNotify(text, desp) {
|
|||||||
timeout,
|
timeout,
|
||||||
};
|
};
|
||||||
$.post(options_accesstoken, (err, resp, data) => {
|
$.post(options_accesstoken, (err, resp, data) => {
|
||||||
html = desp.replace(/\n/g, '<br/>');
|
let html = desp.replace(/\n/g, '<br/>');
|
||||||
var json = JSON.parse(data);
|
let json = JSON.parse(data);
|
||||||
accesstoken = json.access_token;
|
let accesstoken = json.access_token;
|
||||||
let options;
|
let options;
|
||||||
|
|
||||||
switch (QYWX_AM_AY[4]) {
|
switch (QYWX_AM_AY[4]) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ push_config = {
|
|||||||
'BARK_ARCHIVE': '', # bark 推送是否存档
|
'BARK_ARCHIVE': '', # bark 推送是否存档
|
||||||
'BARK_GROUP': '', # bark 推送分组
|
'BARK_GROUP': '', # bark 推送分组
|
||||||
'BARK_SOUND': '', # bark 推送声音
|
'BARK_SOUND': '', # bark 推送声音
|
||||||
|
'BARK_ICON': '', # bark 推送图标
|
||||||
|
|
||||||
'CONSOLE': True, # 控制台输出
|
'CONSOLE': True, # 控制台输出
|
||||||
|
|
||||||
@@ -59,6 +60,8 @@ push_config = {
|
|||||||
|
|
||||||
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
||||||
|
|
||||||
|
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
|
||||||
|
|
||||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||||
|
|
||||||
@@ -104,6 +107,7 @@ def bark(title: str, content: str) -> None:
|
|||||||
"BARK_ARCHIVE": "isArchive",
|
"BARK_ARCHIVE": "isArchive",
|
||||||
"BARK_GROUP": "group",
|
"BARK_GROUP": "group",
|
||||||
"BARK_SOUND": "sound",
|
"BARK_SOUND": "sound",
|
||||||
|
"BARK_ICON": "icon",
|
||||||
}
|
}
|
||||||
params = ""
|
params = ""
|
||||||
for pair in filter(
|
for pair in filter(
|
||||||
@@ -259,6 +263,24 @@ def serverJ(title: str, content: str) -> None:
|
|||||||
print(f'serverJ 推送失败!错误码:{response["message"]}')
|
print(f'serverJ 推送失败!错误码:{response["message"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def pushdeer(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过PushDeer 推送消息
|
||||||
|
"""
|
||||||
|
if not push_config.get("DEER_KEY"):
|
||||||
|
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("PushDeer 服务启动")
|
||||||
|
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
|
||||||
|
url = 'https://api2.pushdeer.com/message/push'
|
||||||
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
|
if len(response.get("content").get("result")) > 0:
|
||||||
|
print("PushDeer 推送成功!")
|
||||||
|
else:
|
||||||
|
print("PushDeer 推送失败!错误信息:", response)
|
||||||
|
|
||||||
|
|
||||||
def pushplus_bot(title: str, content: str) -> None:
|
def pushplus_bot(title: str, content: str) -> None:
|
||||||
"""
|
"""
|
||||||
通过 push+ 推送消息。
|
通过 push+ 推送消息。
|
||||||
@@ -503,6 +525,8 @@ if push_config.get("IGOT_PUSH_KEY"):
|
|||||||
notify_function.append(iGot)
|
notify_function.append(iGot)
|
||||||
if push_config.get("PUSH_KEY"):
|
if push_config.get("PUSH_KEY"):
|
||||||
notify_function.append(serverJ)
|
notify_function.append(serverJ)
|
||||||
|
if push_config.get("DEER_KEY"):
|
||||||
|
notify_function.append(pushdeer)
|
||||||
if push_config.get("PUSH_PLUS_TOKEN"):
|
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
notify_function.append(pushplus_bot)
|
notify_function.append(pushplus_bot)
|
||||||
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import * as qiniu from 'qiniu';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
const envFound = dotenv.config();
|
||||||
|
|
||||||
|
const accessKey = process.env.QINIU_AK;
|
||||||
|
const secretKey = process.env.QINIU_SK;
|
||||||
|
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
|
||||||
|
const key = 'version.ts';
|
||||||
|
const options = {
|
||||||
|
scope: `${process.env.QINIU_SCOPE}:${key}`,
|
||||||
|
};
|
||||||
|
const putPolicy = new qiniu.rs.PutPolicy(options);
|
||||||
|
const uploadToken = putPolicy.uploadToken(mac);
|
||||||
|
|
||||||
|
const localFile = 'src/version.ts';
|
||||||
|
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
|
||||||
|
const formUploader = new qiniu.form_up.FormUploader(config);
|
||||||
|
const putExtra = new qiniu.form_up.PutExtra(
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
'text/plain; charset=utf-8',
|
||||||
|
);
|
||||||
|
// 文件上传
|
||||||
|
formUploader.putFile(
|
||||||
|
uploadToken,
|
||||||
|
key,
|
||||||
|
localFile,
|
||||||
|
putExtra,
|
||||||
|
function (respErr, respBody, respInfo) {
|
||||||
|
if (respErr) {
|
||||||
|
throw respErr;
|
||||||
|
}
|
||||||
|
if (respInfo.statusCode == 200) {
|
||||||
|
console.log(respBody);
|
||||||
|
} else {
|
||||||
|
console.log(respInfo.statusCode);
|
||||||
|
console.log(respBody);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
+33
-25
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
get_token() {
|
get_token() {
|
||||||
token=$(cat $file_auth_user | jq -r .token)
|
token=$(cat $file_auth_token | jq -r .value)
|
||||||
}
|
}
|
||||||
|
|
||||||
add_cron_api() {
|
add_cron_api() {
|
||||||
@@ -17,7 +17,7 @@ add_cron_api() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Authorization: Bearer $token" \
|
-H "Authorization: Bearer $token" \
|
||||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||||
@@ -52,7 +52,7 @@ update_cron_api() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Authorization: Bearer $token" \
|
-H "Authorization: Bearer $token" \
|
||||||
@@ -84,7 +84,7 @@ update_cron_command_api() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Authorization: Bearer $token" \
|
-H "Authorization: Bearer $token" \
|
||||||
@@ -109,7 +109,7 @@ del_cron_api() {
|
|||||||
local ids=$1
|
local ids=$1
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
||||||
-X 'DELETE' \
|
-X 'DELETE' \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Authorization: Bearer $token" \
|
-H "Authorization: Bearer $token" \
|
||||||
@@ -130,24 +130,6 @@ del_cron_api() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
get_user_info() {
|
|
||||||
local currentTimeStamp=$(date +%s)
|
|
||||||
local api=$(
|
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
|
|
||||||
-H 'Accept: */*' \
|
|
||||||
-H "Authorization: Bearer $token" \
|
|
||||||
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
|
|
||||||
-H 'Referer: http://0.0.0.0:5700/crontab' \
|
|
||||||
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
|
|
||||||
--compressed
|
|
||||||
)
|
|
||||||
code=$(echo $api | jq -r .code)
|
|
||||||
if [[ $code != 200 ]]; then
|
|
||||||
echo -e "请先登录!"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
update_cron() {
|
update_cron() {
|
||||||
local ids="$1"
|
local ids="$1"
|
||||||
local status="$2"
|
local status="$2"
|
||||||
@@ -157,7 +139,7 @@ update_cron() {
|
|||||||
local runningTime="${6:-0}"
|
local runningTime="${6:-0}"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Authorization: Bearer $token" \
|
-H "Authorization: Bearer $token" \
|
||||||
@@ -172,7 +154,33 @@ update_cron() {
|
|||||||
code=$(echo $api | jq -r .code)
|
code=$(echo $api | jq -r .code)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo $api | jq -r .message)
|
||||||
if [[ $code != 200 ]]; then
|
if [[ $code != 200 ]]; then
|
||||||
echo -e "\n## 更新任务状态失败(${message})\n" >> $log_path
|
echo -e "\n## 更新任务状态失败(${message})\n" >> $dir_log/$log_path
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_api() {
|
||||||
|
local title=$1
|
||||||
|
local content=$2
|
||||||
|
local currentTimeStamp=$(date +%s)
|
||||||
|
local api=$(
|
||||||
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
|
||||||
|
-X 'PUT' \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||||
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
|
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||||
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
|
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
|
||||||
|
--compressed
|
||||||
|
)
|
||||||
|
code=$(echo $api | jq -r .code)
|
||||||
|
message=$(echo $api | jq -r .message)
|
||||||
|
if [[ $code == 200 ]]; then
|
||||||
|
echo -e "通知发送成功"
|
||||||
|
else
|
||||||
|
echo -e "通知失败(${message})"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 导入通用变量与函数
|
## 导入通用变量与函数
|
||||||
dir_shell=/ql/shell
|
dir_shell=$QL_DIR/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
|
|
||||||
if [[ -z ${BotRepoUrl} ]]; then
|
if [[ -z ${BotRepoUrl} ]]; then
|
||||||
@@ -22,9 +22,9 @@ if [[ ! -d ${repo_path}/.git ]]; then
|
|||||||
git_clone_scripts ${url} ${repo_path} "main"
|
git_clone_scripts ${url} ${repo_path} "main"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cp -rf "$repo_path/jbot" $dir_root
|
cp -rf "$repo_path/jbot" $dir_data
|
||||||
if [[ ! -f "$dir_root/config/bot.json" ]]; then
|
if [[ ! -f "$dir_config/bot.json" ]]; then
|
||||||
cp -f "$repo_path/config/bot.json" "$dir_root/config"
|
cp -f "$repo_path/config/bot.json" "$dir_config"
|
||||||
fi
|
fi
|
||||||
echo -e "\nbot文件下载成功...\n"
|
echo -e "\nbot文件下载成功...\n"
|
||||||
|
|
||||||
@@ -32,9 +32,9 @@ echo -e "3、安装python3依赖...\n"
|
|||||||
if [[ $PipMirror ]]; then
|
if [[ $PipMirror ]]; then
|
||||||
pip3 config set global.index-url $PipMirror
|
pip3 config set global.index-url $PipMirror
|
||||||
fi
|
fi
|
||||||
cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
|
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
|
||||||
|
|
||||||
cd $dir_root
|
cd $dir_data
|
||||||
cat requirements.txt | while read LREAD
|
cat requirements.txt | while read LREAD
|
||||||
do
|
do
|
||||||
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
|
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
|
||||||
@@ -46,7 +46,7 @@ echo -e "\npython3依赖安装成功...\n"
|
|||||||
|
|
||||||
echo -e "4、启动bot程序...\n"
|
echo -e "4、启动bot程序...\n"
|
||||||
make_dir $dir_log/bot
|
make_dir $dir_log/bot
|
||||||
cd $dir_root
|
cd $dir_data
|
||||||
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
|
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
|
||||||
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
|
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
|
||||||
echo -e "bot启动成功...\n"
|
echo -e "bot启动成功...\n"
|
||||||
|
|||||||
+5
-15
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
dir_shell=/ql/shell
|
dir_shell=$QL_DIR/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/api.sh
|
. $dir_shell/api.sh
|
||||||
|
|
||||||
@@ -10,13 +10,11 @@ reset_env() {
|
|||||||
echo -e "---> 配置文件检测完成\n"
|
echo -e "---> 配置文件检测完成\n"
|
||||||
|
|
||||||
echo -e "---> 2. 开始安装青龙依赖\n"
|
echo -e "---> 2. 开始安装青龙依赖\n"
|
||||||
rm -rf $dir_root/node_modules
|
|
||||||
npm_install_2 $dir_root
|
npm_install_2 $dir_root
|
||||||
echo -e "---> 青龙依赖安装完成\n"
|
echo -e "---> 青龙依赖安装完成\n"
|
||||||
|
|
||||||
echo -e "---> 3. 开始安装脚本依赖\n"
|
echo -e "---> 3. 开始安装脚本依赖\n"
|
||||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||||
rm -rf $dir_scripts/node_modules
|
|
||||||
npm_install_2 $dir_scripts
|
npm_install_2 $dir_scripts
|
||||||
echo -e "---> 脚本依赖安装完成\n"
|
echo -e "---> 脚本依赖安装完成\n"
|
||||||
}
|
}
|
||||||
@@ -36,22 +34,12 @@ copy_dep() {
|
|||||||
echo -e "---> 配置文件复制完成\n"
|
echo -e "---> 配置文件复制完成\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
reload_pm2() {
|
|
||||||
pm2 l &>/dev/null
|
|
||||||
|
|
||||||
pm2 delete panel --source-map-support --time &>/dev/null
|
|
||||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
|
|
||||||
|
|
||||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
pm2_log() {
|
pm2_log() {
|
||||||
echo -e "---> pm2日志"
|
echo -e "---> pm2日志"
|
||||||
local panelOut="/root/.pm2/logs/panel-out.log"
|
local panelOut="/root/.pm2/logs/panel-out.log"
|
||||||
local panelError="/root/.pm2/logs/panel-error.log"
|
local panelError="/root/.pm2/logs/panel-error.log"
|
||||||
tail -n 20 "$panelOut"
|
tail -n 100 "$panelOut"
|
||||||
tail -n 20 "$panelError"
|
tail -n 100 "$panelError"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_nginx() {
|
check_nginx() {
|
||||||
@@ -100,6 +88,8 @@ check_pm2() {
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
echo -e "=====> 开始检测"
|
echo -e "=====> 开始检测"
|
||||||
|
npm i -g pnpm
|
||||||
|
pnpm add -g pm2
|
||||||
copy_dep
|
copy_dep
|
||||||
check_ql
|
check_ql
|
||||||
check_nginx
|
check_nginx
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
const notify = require('/ql/scripts/sendNotify.js');
|
|
||||||
const title = process.argv[2];
|
|
||||||
const content = process.argv[3];
|
|
||||||
|
|
||||||
notify.sendNotify(`${title}`, `${content}`);
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#author:spark thanks to: https://github.com/sparkssssssss/scripts
|
|
||||||
|
|
||||||
. /ql/config/config.sh
|
|
||||||
title=$(echo $1|sed 's/-/_/g')
|
|
||||||
msg=$(echo -e $2)
|
|
||||||
|
|
||||||
node /ql/shell/notify.js "$title" "$msg"
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
echo -e "开始发布"
|
||||||
|
|
||||||
|
echo -e "切换master分支"
|
||||||
|
git checkout master
|
||||||
|
|
||||||
|
echo -e "合并develop代码"
|
||||||
|
git merge origin/develop
|
||||||
|
|
||||||
|
echo -e "提交master代码"
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo -e "更新cdn文件"
|
||||||
|
ts-node sample/tool.ts
|
||||||
|
|
||||||
|
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*")
|
||||||
|
version="v$string"
|
||||||
|
echo -e "当前版本$version"
|
||||||
|
|
||||||
|
echo -e "删除已经存在的本地tag"
|
||||||
|
git tag -d "$version" &>/dev/null
|
||||||
|
|
||||||
|
echo -e "删除已经存在的远程tag"
|
||||||
|
git push origin :refs/tags/$version &>/dev/null
|
||||||
|
|
||||||
|
echo -e "创建新tag"
|
||||||
|
git tag -a "$version" -m "release $version"
|
||||||
|
|
||||||
|
echo -e "提交tag"
|
||||||
|
git push --tags
|
||||||
|
|
||||||
|
echo -e "完成发布"
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 导入通用变量与函数
|
## 导入通用变量与函数
|
||||||
dir_shell=/ql/shell
|
dir_shell=$QL_DIR/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
|
|
||||||
days=$1
|
days=$1
|
||||||
|
|||||||
+98
-23
@@ -1,18 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
dir_root=/ql
|
dir_root=$QL_DIR
|
||||||
|
dir_data=$dir_root/data
|
||||||
dir_shell=$dir_root/shell
|
dir_shell=$dir_root/shell
|
||||||
dir_sample=$dir_root/sample
|
dir_sample=$dir_root/sample
|
||||||
dir_config=$dir_root/config
|
dir_static=$dir_root/static
|
||||||
dir_scripts=$dir_root/scripts
|
dir_config=$dir_data/config
|
||||||
dir_repo=$dir_root/repo
|
dir_scripts=$dir_data/scripts
|
||||||
dir_raw=$dir_root/raw
|
dir_repo=$dir_data/repo
|
||||||
dir_log=$dir_root/log
|
dir_raw=$dir_data/raw
|
||||||
dir_db=$dir_root/db
|
dir_log=$dir_data/log
|
||||||
dir_dep=$dir_root/deps
|
dir_db=$dir_data/db
|
||||||
|
dir_dep=$dir_data/deps
|
||||||
dir_list_tmp=$dir_log/.tmp
|
dir_list_tmp=$dir_log/.tmp
|
||||||
dir_code=$dir_log/code
|
|
||||||
dir_update_log=$dir_log/update
|
dir_update_log=$dir_log/update
|
||||||
ql_static_repo=$dir_repo/static
|
ql_static_repo=$dir_repo/static
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ file_sharecode=$dir_config/sharecode.sh
|
|||||||
file_config_user=$dir_config/config.sh
|
file_config_user=$dir_config/config.sh
|
||||||
file_auth_sample=$dir_sample/auth.sample.json
|
file_auth_sample=$dir_sample/auth.sample.json
|
||||||
file_auth_user=$dir_config/auth.json
|
file_auth_user=$dir_config/auth.json
|
||||||
|
file_auth_token=$dir_config/token.json
|
||||||
file_extra_shell=$dir_config/extra.sh
|
file_extra_shell=$dir_config/extra.sh
|
||||||
file_task_before=$dir_config/task_before.sh
|
file_task_before=$dir_config/task_before.sh
|
||||||
file_task_after=$dir_config/task_after.sh
|
file_task_after=$dir_config/task_after.sh
|
||||||
@@ -50,16 +52,14 @@ list_own_drop=$dir_list_tmp/own_drop.list
|
|||||||
link_name=(
|
link_name=(
|
||||||
task
|
task
|
||||||
ql
|
ql
|
||||||
notify
|
|
||||||
)
|
)
|
||||||
original_name=(
|
original_name=(
|
||||||
task.sh
|
task.sh
|
||||||
update.sh
|
update.sh
|
||||||
notify.sh
|
|
||||||
)
|
)
|
||||||
|
|
||||||
init_env() {
|
init_env() {
|
||||||
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules
|
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
|
||||||
export PYTHONUNBUFFERED=1
|
export PYTHONUNBUFFERED=1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ make_dir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
detect_termux() {
|
detect_termux() {
|
||||||
if [[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] || [[ $PATH == *com.termux* ]]; then
|
if [[ $PATH == *com.termux* ]]; then
|
||||||
is_termux=1
|
is_termux=1
|
||||||
else
|
else
|
||||||
is_termux=0
|
is_termux=0
|
||||||
@@ -163,6 +163,8 @@ define_cmd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fix_config() {
|
fix_config() {
|
||||||
|
make_dir $dir_static
|
||||||
|
make_dir $dir_data
|
||||||
make_dir $dir_config
|
make_dir $dir_config
|
||||||
make_dir $dir_log
|
make_dir $dir_log
|
||||||
make_dir $dir_db
|
make_dir $dir_db
|
||||||
@@ -242,13 +244,15 @@ fix_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
npm_install_sub() {
|
npm_install_sub() {
|
||||||
|
set_proxy
|
||||||
if [ $is_termux -eq 1 ]; then
|
if [ $is_termux -eq 1 ]; then
|
||||||
npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
|
npm install --production --no-bin-links --registry=https://registry.npmmirror.com || npm install --production --no-bin-links
|
||||||
elif ! type pnpm &>/dev/null; then
|
elif ! type pnpm &>/dev/null; then
|
||||||
npm install --production --registry=https://registry.npm.taobao.org || npm install --production
|
npm install --production --registry=https://registry.npmmirror.com || npm install --production
|
||||||
else
|
else
|
||||||
pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
|
pnpm install --loglevel error --production --registry=https://registry.npmmirror.com || pnpm install --production --loglevel error
|
||||||
fi
|
fi
|
||||||
|
unset_proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
npm_install_1() {
|
npm_install_1() {
|
||||||
@@ -312,16 +316,15 @@ git_pull_scripts() {
|
|||||||
local dir_current=$(pwd)
|
local dir_current=$(pwd)
|
||||||
local dir_work="$1"
|
local dir_work="$1"
|
||||||
local branch="$2"
|
local branch="$2"
|
||||||
[[ $branch ]] && local part_cmd="origin/${branch}"
|
|
||||||
cd $dir_work
|
cd $dir_work
|
||||||
echo -e "开始更新仓库:$dir_work\n"
|
echo -e "开始更新仓库:$dir_work\n"
|
||||||
|
|
||||||
set_proxy
|
set_proxy
|
||||||
git fetch --all
|
git fetch --all
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
git reset --hard $part_cmd
|
git pull &>/dev/null
|
||||||
git pull
|
|
||||||
unset_proxy
|
unset_proxy
|
||||||
|
reset_branch "$branch"
|
||||||
|
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
@@ -332,23 +335,95 @@ reset_romote_url() {
|
|||||||
local url=$2
|
local url=$2
|
||||||
local branch="$3"
|
local branch="$3"
|
||||||
|
|
||||||
[[ $branch ]] && local part_cmd="origin/${branch}"
|
|
||||||
|
|
||||||
if [[ -d "$dir_work/.git" ]]; then
|
if [[ -d "$dir_work/.git" ]]; then
|
||||||
cd $dir_work
|
cd $dir_work
|
||||||
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
||||||
git remote set-url origin $url >/dev/null
|
git remote set-url origin $url &>/dev/null
|
||||||
git reset --hard $part_cmd >/dev/null
|
|
||||||
|
local part_cmd=""
|
||||||
|
reset_branch "$branch"
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset_branch() {
|
||||||
|
local branch="$1"
|
||||||
|
if [[ $branch ]]; then
|
||||||
|
part_cmd="origin/${branch}"
|
||||||
|
git checkout -B "$branch"
|
||||||
|
git branch --set-upstream-to=$part_cmd $branch
|
||||||
|
fi
|
||||||
|
git reset --hard $part_cmd &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
random_range() {
|
random_range() {
|
||||||
local beg=$1
|
local beg=$1
|
||||||
local end=$2
|
local end=$2
|
||||||
echo $((RANDOM % ($end - $beg) + $beg))
|
echo $((RANDOM % ($end - $beg) + $beg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reload_pm2() {
|
||||||
|
pm2 l &>/dev/null
|
||||||
|
|
||||||
|
echo -e "启动面板服务\n"
|
||||||
|
pm2 delete panel --source-map-support --time &>/dev/null
|
||||||
|
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||||
|
|
||||||
|
echo -e "启动定时任务服务\n"
|
||||||
|
pm2 delete schedule --source-map-support --time &>/dev/null
|
||||||
|
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||||
|
|
||||||
|
echo -e "启动公开服务\n"
|
||||||
|
pm2 delete public --source-map-support --time &>/dev/null
|
||||||
|
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
diff_time() {
|
||||||
|
local format="$1"
|
||||||
|
local begin_time="$2"
|
||||||
|
local end_time="$3"
|
||||||
|
|
||||||
|
if [[ $is_macos -eq 1 ]]; then
|
||||||
|
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
|
||||||
|
else
|
||||||
|
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
||||||
|
fi
|
||||||
|
echo "$diff_time"
|
||||||
|
}
|
||||||
|
|
||||||
|
format_time() {
|
||||||
|
local format="$1"
|
||||||
|
local time="$2"
|
||||||
|
|
||||||
|
if [[ $is_macos -eq 1 ]]; then
|
||||||
|
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
|
||||||
|
else
|
||||||
|
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
format_log_time() {
|
||||||
|
local format="$1"
|
||||||
|
local time="$2"
|
||||||
|
|
||||||
|
if [[ $is_macos -eq 1 ]]; then
|
||||||
|
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
|
||||||
|
else
|
||||||
|
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
format_timestamp() {
|
||||||
|
local format="$1"
|
||||||
|
local time="$2"
|
||||||
|
|
||||||
|
if [[ $is_macos -eq 1 ]]; then
|
||||||
|
echo $(date -j -f "$format" "$time" "+%s")
|
||||||
|
else
|
||||||
|
echo $(date -d "$time" "+%s")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
init_env
|
init_env
|
||||||
detect_termux
|
detect_termux
|
||||||
detect_macos
|
detect_macos
|
||||||
|
|||||||
+99
-61
@@ -1,20 +1,20 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 导入通用变量与函数
|
## 导入通用变量与函数
|
||||||
dir_shell=/ql/shell
|
dir_shell=$QL_DIR/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/api.sh
|
. $dir_shell/api.sh
|
||||||
|
|
||||||
## 选择python3还是node
|
## 选择python3还是node
|
||||||
define_program() {
|
define_program() {
|
||||||
local first_param=$1
|
local file_param=$1
|
||||||
if [[ $first_param == *.js ]]; then
|
if [[ $file_param == *.js ]]; then
|
||||||
which_program="node"
|
which_program="node"
|
||||||
elif [[ $first_param == *.py ]] || [[ $first_param == *.pyc ]]; then
|
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
|
||||||
which_program="python3"
|
which_program="python3"
|
||||||
elif [[ $first_param == *.sh ]]; then
|
elif [[ $file_param == *.sh ]]; then
|
||||||
which_program="bash"
|
which_program="bash"
|
||||||
elif [[ $first_param == *.ts ]]; then
|
elif [[ $file_param == *.ts ]]; then
|
||||||
which_program="ts-node-transpile-only"
|
which_program="ts-node-transpile-only"
|
||||||
else
|
else
|
||||||
which_program=""
|
which_program=""
|
||||||
@@ -76,53 +76,63 @@ run_nohup() {
|
|||||||
|
|
||||||
## 正常运行单个脚本,$1:传入参数
|
## 正常运行单个脚本,$1:传入参数
|
||||||
run_normal() {
|
run_normal() {
|
||||||
local first_param=$1
|
local file_param=$1
|
||||||
define_program "$first_param"
|
define_program "$file_param"
|
||||||
if [[ $first_param == *.js ]]; then
|
if [[ $file_param == *.js ]]; then
|
||||||
if [[ $# -eq 1 ]]; then
|
if [[ $# -eq 1 ]]; then
|
||||||
random_delay
|
random_delay
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
local time=$(date "+$time_format")
|
||||||
log_dir_tmp="${first_param##*/}"
|
log_time=$(format_log_time "$time_format" "$time")
|
||||||
log_dir_tmp_path="${first_param%%/*}"
|
log_dir_tmp="${file_param##*/}"
|
||||||
|
if [[ $file_param =~ "/" ]]; then
|
||||||
|
if [[ $file_param == /* ]]; then
|
||||||
|
log_dir_tmp_path="${file_param:1}"
|
||||||
|
else
|
||||||
|
log_dir_tmp_path="${file_param}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
log_dir="${log_dir_tmp%.*}"
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
cmd="&>> $log_path"
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
[[ "$show_log" == "true" ]] && cmd=""
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
make_dir "$log_dir"
|
make_dir "$dir_log/$log_dir"
|
||||||
|
|
||||||
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
|
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
local begin_timestamp=$(date "+%s")
|
|
||||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
eval . $file_task_before "$@" $cmd
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
local relative_path="${first_param%/*}"
|
local relative_path="${file_param%/*}"
|
||||||
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
|
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||||
cd ${relative_path}
|
cd ${relative_path}
|
||||||
first_param=${first_param/$relative_path\//}
|
file_param=${file_param/$relative_path\//}
|
||||||
fi
|
fi
|
||||||
eval timeout -k 10s $command_timeout_time $which_program $first_param $cmd
|
|
||||||
|
eval $timeoutCmd $which_program $file_param $cmd
|
||||||
|
|
||||||
eval . $file_task_after "$@" $cmd
|
eval . $file_task_after "$@" $cmd
|
||||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
local end_timestamp=$(date "+%s")
|
local end_timestamp=$(date "+%s")
|
||||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
local diff_time=$(expr $end_timestamp - $begin_timestamp)
|
||||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||||
run_concurrent() {
|
run_concurrent() {
|
||||||
local first_param="$1"
|
local file_param="$1"
|
||||||
local env_param="$2"
|
local env_param="$2"
|
||||||
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
||||||
if [[ ! $env_param ]]; then
|
if [[ ! $env_param ]]; then
|
||||||
@@ -143,50 +153,57 @@ run_concurrent() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||||
|
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||||
|
|
||||||
define_program "$first_param"
|
define_program "$file_param"
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
local time=$(date "+$time_format")
|
||||||
log_dir_tmp="${first_param##*/}"
|
log_time=$(format_log_time "$time_format" "$time")
|
||||||
log_dir_tmp_path="${first_param%%/*}"
|
log_dir_tmp="${file_param##*/}"
|
||||||
|
if [[ $file_param =~ "/" ]]; then
|
||||||
|
if [[ $file_param == /* ]]; then
|
||||||
|
log_dir_tmp_path="${file_param:1}"
|
||||||
|
else
|
||||||
|
log_dir_tmp_path="${file_param}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
log_dir="${log_dir_tmp%.*}"
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
cmd="&>> $log_path"
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
[[ "$show_log" == "true" ]] && cmd=""
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
make_dir $log_dir
|
make_dir "$dir_log/$log_dir"
|
||||||
|
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
local begin_timestamp=$(date "+%s")
|
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
|
||||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
eval . $file_task_before "$@" $cmd
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
|
||||||
|
|
||||||
local envs=$(eval echo "\$${env_param}")
|
local envs=$(eval echo "\$${env_param}")
|
||||||
local array=($(echo $envs | sed 's/&/ /g'))
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
||||||
|
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
local relative_path="${first_param%/*}"
|
local relative_path="${file_param%/*}"
|
||||||
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
|
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||||
cd ${relative_path}
|
cd ${relative_path}
|
||||||
first_param=${first_param/$relative_path\//}
|
file_param=${file_param/$relative_path\//}
|
||||||
fi
|
fi
|
||||||
for i in "${!array[@]}"; do
|
for i in "${!array[@]}"; do
|
||||||
export ${env_param}=${array[i]}
|
export ${env_param}=${array[i]}
|
||||||
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||||
timeout -k 10s $command_timeout_time $which_program $first_param &>$single_log_path &
|
eval $timeoutCmd $which_program $file_param &>$single_log_path &
|
||||||
done
|
done
|
||||||
|
|
||||||
wait
|
wait
|
||||||
for i in "${!array[@]}"; do
|
for i in "${!array[@]}"; do
|
||||||
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||||
eval cat $single_log_path $cmd
|
eval cat $single_log_path $cmd
|
||||||
[[ -f $single_log_path ]] && rm -f $single_log_path
|
[[ -f $single_log_path ]] && rm -f $single_log_path
|
||||||
done
|
done
|
||||||
@@ -209,19 +226,27 @@ run_designated() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
define_program "$file_param"
|
define_program "$file_param"
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
local time=$(date "+$time_format")
|
||||||
|
log_time=$(format_log_time "$time_format" "$time")
|
||||||
log_dir_tmp="${file_param##*/}"
|
log_dir_tmp="${file_param##*/}"
|
||||||
log_dir_tmp_path="${file_param%%/*}"
|
if [[ $file_param =~ "/" ]]; then
|
||||||
|
if [[ $file_param == /* ]]; then
|
||||||
|
log_dir_tmp_path="${file_param:1}"
|
||||||
|
else
|
||||||
|
log_dir_tmp_path="${file_param}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
log_dir="${log_dir_tmp%.*}"
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
cmd="&>> $log_path"
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
[[ "$show_log" == "true" ]] && cmd=""
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
make_dir $log_dir
|
make_dir "$dir_log/$log_dir"
|
||||||
|
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
local begin_timestamp=$(date "+%s")
|
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
|
||||||
local envs=$(eval echo "\$${env_param}")
|
local envs=$(eval echo "\$${env_param}")
|
||||||
local array=($(echo $envs | sed 's/&/ /g'))
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
@@ -236,6 +261,7 @@ run_designated() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||||
|
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||||
|
|
||||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
@@ -244,15 +270,13 @@ run_designated() {
|
|||||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
eval . $file_task_before "$@" $cmd
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
|
||||||
|
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
local relative_path="${file_param%/*}"
|
local relative_path="${file_param%/*}"
|
||||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||||
cd ${relative_path}
|
cd ${relative_path}
|
||||||
file_param=${file_param/$relative_path\//}
|
file_param=${file_param/$relative_path\//}
|
||||||
fi
|
fi
|
||||||
eval timeout -k 10s $command_timeout_time $which_program $file_param $cmd
|
eval $timeoutCmd $which_program $file_param $cmd
|
||||||
|
|
||||||
eval . $file_task_after "$@" $cmd
|
eval . $file_task_after "$@" $cmd
|
||||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
@@ -266,19 +290,27 @@ run_designated() {
|
|||||||
run_else() {
|
run_else() {
|
||||||
local file_param="$1"
|
local file_param="$1"
|
||||||
define_program "$file_param"
|
define_program "$file_param"
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
local time=$(date "+$time_format")
|
||||||
|
log_time=$(format_log_time "$time_format" "$time")
|
||||||
log_dir_tmp="${file_param##*/}"
|
log_dir_tmp="${file_param##*/}"
|
||||||
log_dir_tmp_path="${file_param%%/*}"
|
if [[ $file_param =~ "/" ]]; then
|
||||||
|
if [[ $file_param == /* ]]; then
|
||||||
|
log_dir_tmp_path="${file_param:1}"
|
||||||
|
else
|
||||||
|
log_dir_tmp_path="${file_param}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
log_dir="${log_dir_tmp%.*}"
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
cmd="&>> $log_path"
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
[[ "$show_log" == "true" ]] && cmd=""
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
make_dir $log_dir
|
make_dir "$dir_log/$log_dir"
|
||||||
|
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
local begin_timestamp=$(date "+%s")
|
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
|
||||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
@@ -295,7 +327,7 @@ run_else() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
shift
|
shift
|
||||||
eval timeout -k 10s $command_timeout_time $which_program "$file_param" "$@" $cmd
|
eval $timeoutCmd $which_program "$file_param" "$@" $cmd
|
||||||
|
|
||||||
eval . $file_task_after "$file_param" "$@" $cmd
|
eval . $file_task_after "$file_param" "$@" $cmd
|
||||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
@@ -318,6 +350,12 @@ main() {
|
|||||||
done
|
done
|
||||||
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
||||||
|
|
||||||
|
timeoutCmd=""
|
||||||
|
if type timeout &>/dev/null; then
|
||||||
|
timeoutCmd="timeout -k 10s $command_timeout_time "
|
||||||
|
fi
|
||||||
|
|
||||||
|
time_format="%Y-%m-%d %H:%M:%S"
|
||||||
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
|
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
|
||||||
case $# in
|
case $# in
|
||||||
1)
|
1)
|
||||||
@@ -340,7 +378,7 @@ main() {
|
|||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
[[ -f $log_path ]] && cat $log_path
|
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
|
||||||
elif [[ $# -eq 0 ]]; then
|
elif [[ $# -eq 0 ]]; then
|
||||||
echo
|
echo
|
||||||
usage
|
usage
|
||||||
|
|||||||
+77
-56
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
dir_shell=/ql/shell
|
dir_shell=$QL_DIR/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/api.sh
|
. $dir_shell/api.sh
|
||||||
|
|
||||||
@@ -12,14 +12,16 @@ diff_cron() {
|
|||||||
local list_task="$2"
|
local list_task="$2"
|
||||||
local list_add="$3"
|
local list_add="$3"
|
||||||
local list_drop="$4"
|
local list_drop="$4"
|
||||||
if [[ -s $list_task ]]; then
|
if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then
|
||||||
grep -vwf $list_task $list_scripts >$list_add
|
grep -vwf $list_task $list_scripts >$list_add
|
||||||
elif [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
|
grep -vwf $list_scripts $list_task >$list_drop
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
|
||||||
cp -f $list_scripts $list_add
|
cp -f $list_scripts $list_add
|
||||||
fi
|
fi
|
||||||
if [[ -s $list_scripts ]]; then
|
|
||||||
grep -vwf $list_scripts $list_task >$list_drop
|
if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then
|
||||||
else
|
|
||||||
cp -f $list_task $list_drop
|
cp -f $list_task $list_drop
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -43,7 +45,7 @@ detect_config_version() {
|
|||||||
local notify_title="配置文件更新通知"
|
local notify_title="配置文件更新通知"
|
||||||
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
|
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
|
||||||
echo -e $notify_content
|
echo -e $notify_content
|
||||||
notify "$notify_title" "$notify_content"
|
notify_api "$notify_title" "$notify_content"
|
||||||
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
|
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -91,7 +93,7 @@ del_cron() {
|
|||||||
done
|
done
|
||||||
if [[ $ids ]]; then
|
if [[ $ids ]]; then
|
||||||
result=$(del_cron_api "$ids")
|
result=$(del_cron_api "$ids")
|
||||||
notify "$path 删除任务${result}" "$detail"
|
notify_api "$path 删除任务${result}" "$detail"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ add_cron() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
notify "$path 新增任务" "$detail"
|
notify_api "$path 新增任务" "$detail"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新仓库
|
## 更新仓库
|
||||||
@@ -141,6 +143,7 @@ update_repo() {
|
|||||||
local blackword="$3"
|
local blackword="$3"
|
||||||
local dependence="$4"
|
local dependence="$4"
|
||||||
local branch="$5"
|
local branch="$5"
|
||||||
|
local extensions="$6"
|
||||||
local tmp="${url%/*}"
|
local tmp="${url%/*}"
|
||||||
local authorTmp1="${tmp##*/}"
|
local authorTmp1="${tmp##*/}"
|
||||||
local authorTmp2="${authorTmp1##*:}"
|
local authorTmp2="${authorTmp1##*:}"
|
||||||
@@ -159,18 +162,10 @@ update_repo() {
|
|||||||
fi
|
fi
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新${repo_path}成功...\n"
|
echo -e "\n更新${repo_path}成功...\n"
|
||||||
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
|
||||||
else
|
|
||||||
echo -e "\n更新${repo_path}失败,重新下载全新仓库...\n"
|
|
||||||
rm -rf ${repo_path}
|
|
||||||
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
|
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
|
||||||
echo -e "\n更新${repo_path}成功...\n"
|
|
||||||
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
|
||||||
else
|
else
|
||||||
echo -e "\n更新${repo_path}失败,请检查网络...\n"
|
echo -e "\n更新${repo_path}失败,请检查网络...\n"
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新所有 raw 文件
|
## 更新所有 raw 文件
|
||||||
@@ -211,7 +206,7 @@ update_raw() {
|
|||||||
if [[ -z $cron_id ]]; then
|
if [[ -z $cron_id ]]; then
|
||||||
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
|
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
|
||||||
echo -e "$result\n"
|
echo -e "$result\n"
|
||||||
notify "新增任务通知" "\n$result"
|
notify_api "新增任务通知" "\n$result"
|
||||||
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -239,7 +234,7 @@ usage() {
|
|||||||
echo -e "1. $cmd_update update # 更新并重启青龙"
|
echo -e "1. $cmd_update update # 更新并重启青龙"
|
||||||
echo -e "2. $cmd_update extra # 运行自定义脚本"
|
echo -e "2. $cmd_update extra # 运行自定义脚本"
|
||||||
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
|
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
|
||||||
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本"
|
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
|
||||||
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
|
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
|
||||||
echo -e "6. $cmd_update bot # 启动tg-bot"
|
echo -e "6. $cmd_update bot # 启动tg-bot"
|
||||||
echo -e "7. $cmd_update check # 检测青龙环境并修复"
|
echo -e "7. $cmd_update check # 检测青龙环境并修复"
|
||||||
@@ -252,12 +247,17 @@ update_qinglong() {
|
|||||||
patch_version
|
patch_version
|
||||||
|
|
||||||
local no_restart="$1"
|
local no_restart="$1"
|
||||||
|
local all_branch=$(git branch -a)
|
||||||
|
local primary_branch="master"
|
||||||
|
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
|
||||||
|
primary_branch="${current_branch}"
|
||||||
|
fi
|
||||||
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
|
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
|
||||||
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${current_branch}
|
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
|
||||||
git_pull_scripts $dir_root ${current_branch}
|
git_pull_scripts $dir_root ${primary_branch}
|
||||||
|
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新$dir_root成功...\n"
|
echo -e "\n更新青龙源文件成功...\n"
|
||||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||||
detect_config_version
|
detect_config_version
|
||||||
update_depend
|
update_depend
|
||||||
@@ -265,23 +265,23 @@ update_qinglong() {
|
|||||||
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
||||||
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
||||||
else
|
else
|
||||||
echo -e "\n更新$dir_root失败,请检查原因...\n"
|
echo -e "\n更新青龙源文件失败,请检查原因...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local url="https://github.com/whyour/qinglong-static.git"
|
local url="https://github.com/whyour/qinglong-static.git"
|
||||||
if [[ -d ${ql_static_repo}/.git ]]; then
|
if [[ -d ${ql_static_repo}/.git ]]; then
|
||||||
reset_romote_url ${ql_static_repo} ${url} ${current_branch}
|
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
|
||||||
git_pull_scripts ${ql_static_repo} ${current_branch}
|
git_pull_scripts ${ql_static_repo} ${primary_branch}
|
||||||
else
|
else
|
||||||
git_clone_scripts ${url} ${ql_static_repo}
|
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
|
||||||
fi
|
fi
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新$ql_static_repo成功...\n"
|
echo -e "\n更新青龙静态资源成功...\n"
|
||||||
local static_version=$(cat /ql/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
|
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
|
||||||
echo -e "\n当前版本 $static_version...\n"
|
echo -e "\n当前版本 $static_version...\n"
|
||||||
cd $dir_root
|
|
||||||
rm -rf $dir_root/build && rm -rf $dir_root/dist
|
rm -rf $dir_static/*
|
||||||
cp -rf $ql_static_repo/* $dir_root
|
cp -rf $ql_static_repo/* $dir_static
|
||||||
if [[ $no_restart != "no-restart" ]]; then
|
if [[ $no_restart != "no-restart" ]]; then
|
||||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||||
echo -e "重启面板中..."
|
echo -e "重启面板中..."
|
||||||
@@ -289,16 +289,16 @@ update_qinglong() {
|
|||||||
reload_pm2
|
reload_pm2
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "\n更新$dir_root失败,请检查原因...\n"
|
echo -e "\n更新青龙静态资源失败,请检查原因...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
patch_version() {
|
patch_version() {
|
||||||
if [[ -f "/ql/db/cookie.db" ]]; then
|
if [[ -f "$dir_root/db/cookie.db" ]]; then
|
||||||
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
||||||
mv /ql/db/cookie.db /ql/db/env.db
|
mv $dir_root/db/cookie.db $dir_root/db/env.db
|
||||||
rm -rf /ql/db/cookie.db
|
rm -rf $dir_root/db/cookie.db
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -306,19 +306,38 @@ patch_version() {
|
|||||||
pnpm i -g ts-node typescript tslib
|
pnpm i -g ts-node typescript tslib
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 兼容pnpm@7
|
||||||
|
pnpm setup
|
||||||
|
source ~/.bashrc
|
||||||
|
|
||||||
git config --global pull.rebase false
|
git config --global pull.rebase false
|
||||||
|
|
||||||
cp -f /ql/.env.example /ql/.env
|
cp -f $dir_root/.env.example $dir_root/.env
|
||||||
}
|
|
||||||
|
|
||||||
reload_pm2() {
|
if [[ -d "$dir_root/db" ]]; then
|
||||||
pm2 l &>/dev/null
|
echo -e "检测到旧的db目录,拷贝到data目录...\n"
|
||||||
|
cp -rf $dir_root/config $dir_root/data
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
pm2 delete panel --source-map-support --time &>/dev/null
|
if [[ -d "$dir_root/scripts" ]]; then
|
||||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
|
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
|
||||||
|
cp -rf $dir_root/scripts $dir_root/data
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$dir_root/log" ]]; then
|
||||||
|
echo -e "检测到旧的log目录,拷贝到data目录...\n"
|
||||||
|
cp -rf $dir_root/log $dir_root/data
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$dir_root/config" ]]; then
|
||||||
|
echo -e "检测到旧的config目录,拷贝到data目录...\n"
|
||||||
|
cp -rf $dir_root/config $dir_root/data
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## 对比脚本
|
## 对比脚本
|
||||||
@@ -329,8 +348,9 @@ diff_scripts() {
|
|||||||
local path="$3"
|
local path="$3"
|
||||||
local blackword="$4"
|
local blackword="$4"
|
||||||
local dependence="$5"
|
local dependence="$5"
|
||||||
|
local extensions="$6"
|
||||||
|
|
||||||
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
|
||||||
|
|
||||||
local list_add="$dir_list_tmp/${uniq_path}_add.list"
|
local list_add="$dir_list_tmp/${uniq_path}_add.list"
|
||||||
local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
|
local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
|
||||||
@@ -366,6 +386,12 @@ gen_list_repo() {
|
|||||||
|
|
||||||
local cmd="find ."
|
local cmd="find ."
|
||||||
local index=0
|
local index=0
|
||||||
|
if [[ $6 ]]; then
|
||||||
|
file_extensions="$6"
|
||||||
|
if [[ $file_extensions =~ "|" ]]; then
|
||||||
|
file_extensions=$(echo $file_extensions | sed 's/|/ /g')
|
||||||
|
fi
|
||||||
|
fi
|
||||||
for extension in $file_extensions; do
|
for extension in $file_extensions; do
|
||||||
if [[ $index -eq 0 ]]; then
|
if [[ $index -eq 0 ]]; then
|
||||||
cmd="${cmd} -name \"*.${extension}\""
|
cmd="${cmd} -name \"*.${extension}\""
|
||||||
@@ -446,6 +472,7 @@ main() {
|
|||||||
local p4=$4
|
local p4=$4
|
||||||
local p5=$5
|
local p5=$5
|
||||||
local p6=$6
|
local p6=$6
|
||||||
|
local p7=$7
|
||||||
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||||
local log_path="$dir_log/update/${log_time}_$p1.log"
|
local log_path="$dir_log/update/${log_time}_$p1.log"
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
@@ -464,26 +491,18 @@ main() {
|
|||||||
run_extra_shell >>$log_path
|
run_extra_shell >>$log_path
|
||||||
;;
|
;;
|
||||||
repo)
|
repo)
|
||||||
get_user_info
|
|
||||||
get_uniq_path "$p2" "$p6"
|
get_uniq_path "$p2" "$p6"
|
||||||
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
|
|
||||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
|
||||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
|
||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path
|
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7"
|
||||||
else
|
else
|
||||||
echo -e "命令输入错误...\n"
|
echo -e "命令输入错误...\n"
|
||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
raw)
|
raw)
|
||||||
get_user_info
|
|
||||||
get_uniq_path "$p2"
|
get_uniq_path "$p2"
|
||||||
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
|
|
||||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
|
||||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
|
||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_raw "$p2" >>$log_path
|
update_raw "$p2"
|
||||||
else
|
else
|
||||||
echo -e "命令输入错误...\n"
|
echo -e "命令输入错误...\n"
|
||||||
usage
|
usage
|
||||||
@@ -522,9 +541,11 @@ main() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
local diff_time=$(diff_time "%Y-%m-%d %H:%M:%S" "$begin_time" "$end_time")
|
||||||
|
if [[ $p1 != "repo" ]] && [[ $p1 != "raw" ]]; then
|
||||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒" >>$log_path
|
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒" >>$log_path
|
||||||
cat $log_path
|
cat $log_path
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFromIconfontCN } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const IconFont = createFromIconfontCN({
|
||||||
|
scriptUrl: ['//at.alicdn.com/t/font_3354854_ds8pa06q1qa.js'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default IconFont;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
.react-terminal-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
background: #252a33;
|
||||||
|
color: #eee;
|
||||||
|
font-size: 18px;
|
||||||
|
font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier,
|
||||||
|
monospace;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 75px 45px 35px;
|
||||||
|
position: relative;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal {
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal-wrapper.react-terminal-light {
|
||||||
|
background: #ddd;
|
||||||
|
color: #1a1e24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal-wrapper:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #d9515d;
|
||||||
|
-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
|
||||||
|
box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal-wrapper:after {
|
||||||
|
content: attr(data-terminal-name);
|
||||||
|
position: absolute;
|
||||||
|
color: #a2a2a2;
|
||||||
|
top: 5px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal-wrapper.react-terminal-light:after {
|
||||||
|
color: #d76d77;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal-line {
|
||||||
|
display: block;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal-line:before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: #a2a2a2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal-light .react-terminal-line:before {
|
||||||
|
color: #d76d77;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal-input:before {
|
||||||
|
margin-right: 0.75em;
|
||||||
|
content: '$';
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-terminal-input[data-terminal-prompt]:before {
|
||||||
|
content: attr(data-terminal-prompt);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { Tag, Input } from 'antd';
|
||||||
|
import { TweenOneGroup } from 'rc-tween-one';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
const EditableTagGroup = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value?: string[];
|
||||||
|
onChange?: (tags: string[]) => void;
|
||||||
|
}) => {
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [inputVisible, setInputVisible] = useState(false);
|
||||||
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
|
const saveInputRef = useRef<any>();
|
||||||
|
|
||||||
|
const handleClose = (removedTag: string) => {
|
||||||
|
const _tags = tags.filter((tag) => tag !== removedTag);
|
||||||
|
setTags(_tags);
|
||||||
|
onChange?.(_tags);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showInput = () => {
|
||||||
|
setInputVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
setInputValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputConfirm = () => {
|
||||||
|
if (inputValue && !tags.includes(inputValue)) {
|
||||||
|
setTags([...tags, inputValue]);
|
||||||
|
onChange?.([...tags, inputValue]);
|
||||||
|
}
|
||||||
|
setInputVisible(false);
|
||||||
|
setInputValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagChild = tags.map((tag) => {
|
||||||
|
const tagElem = (
|
||||||
|
<Tag
|
||||||
|
closable
|
||||||
|
onClose={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleClose(tag);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
|
||||||
|
{tagElem}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputVisible && saveInputRef) {
|
||||||
|
saveInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [inputVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
setTags(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TweenOneGroup
|
||||||
|
enter={{
|
||||||
|
scale: 0.8,
|
||||||
|
opacity: 0,
|
||||||
|
type: 'from',
|
||||||
|
duration: 100,
|
||||||
|
}}
|
||||||
|
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
|
||||||
|
appear={false}
|
||||||
|
>
|
||||||
|
{tagChild}
|
||||||
|
</TweenOneGroup>
|
||||||
|
{inputVisible && (
|
||||||
|
<Input
|
||||||
|
ref={saveInputRef}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
style={{ width: 78 }}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleInputConfirm}
|
||||||
|
onPressEnter={handleInputConfirm}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!inputVisible && (
|
||||||
|
<Tag
|
||||||
|
onClick={showInput}
|
||||||
|
style={{ borderStyle: 'dashed', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<PlusOutlined /> 新建
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditableTagGroup;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
export enum LineType {
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ColorMode {
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
name?: string;
|
||||||
|
prompt?: string;
|
||||||
|
colorMode?: ColorMode;
|
||||||
|
lineData: Array<{ type: LineType; value: string | React.ReactNode }>;
|
||||||
|
startingInputValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Terminal = ({
|
||||||
|
name,
|
||||||
|
prompt,
|
||||||
|
colorMode,
|
||||||
|
lineData,
|
||||||
|
startingInputValue = '',
|
||||||
|
}: Props) => {
|
||||||
|
const lastLineRef = useRef<null | HTMLElement>(null);
|
||||||
|
|
||||||
|
// An effect that handles scrolling into view the last line of terminal input or output
|
||||||
|
const performScrolldown = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (performScrolldown.current) {
|
||||||
|
// skip scrolldown when the component first loads
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
lastLineRef?.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'nearest',
|
||||||
|
}),
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
performScrolldown.current = true;
|
||||||
|
}, [lineData.length]);
|
||||||
|
|
||||||
|
const renderedLineData = lineData.map((ld, i) => {
|
||||||
|
const classes = ['react-terminal-line'];
|
||||||
|
if (ld.type === LineType.Input) {
|
||||||
|
classes.push('react-terminal-input');
|
||||||
|
}
|
||||||
|
// `lastLineRef` is used to ensure the terminal scrolls into view to the last line; make sure to add the ref to the last
|
||||||
|
if (lineData.length === i + 1) {
|
||||||
|
return (
|
||||||
|
<span className={classes.join(' ')} key={i} ref={lastLineRef}>
|
||||||
|
{ld.value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span className={classes.join(' ')} key={i}>
|
||||||
|
{ld.value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = ['react-terminal-wrapper'];
|
||||||
|
if (colorMode === ColorMode.Light) {
|
||||||
|
classes.push('react-terminal-light');
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={classes.join(' ')} data-terminal-name={name}>
|
||||||
|
<div className="react-terminal">{renderedLineData}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Terminal;
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
ContainerOutlined,
|
ContainerOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import IconFont from '@/components/iconfont';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
route: {
|
route: {
|
||||||
@@ -25,46 +26,58 @@ export default {
|
|||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: '@/pages/initialization/index',
|
component: '@/pages/initialization/index',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '错误',
|
||||||
|
path: '/error',
|
||||||
|
hideInMenu: true,
|
||||||
|
component: '@/pages/error/index',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/crontab',
|
path: '/crontab',
|
||||||
name: '定时任务',
|
name: '定时任务',
|
||||||
icon: <FieldTimeOutlined />,
|
icon: <IconFont type="ql-icon-crontab" />,
|
||||||
component: '@/pages/crontab/index',
|
component: '@/pages/crontab/index',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/subscription',
|
||||||
|
name: '订阅管理',
|
||||||
|
icon: <IconFont type="ql-icon-subs" />,
|
||||||
|
component: '@/pages/subscription/index',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/env',
|
path: '/env',
|
||||||
name: '环境变量',
|
name: '环境变量',
|
||||||
icon: <RadiusSettingOutlined />,
|
icon: <IconFont type="ql-icon-env" />,
|
||||||
component: '@/pages/env/index',
|
component: '@/pages/env/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/config',
|
path: '/config',
|
||||||
name: '配置文件',
|
name: '配置文件',
|
||||||
icon: <ControlOutlined />,
|
icon: <IconFont type="ql-icon-config" />,
|
||||||
component: '@/pages/config/index',
|
component: '@/pages/config/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/script',
|
path: '/script',
|
||||||
name: '脚本管理',
|
name: '脚本管理',
|
||||||
icon: <FormOutlined />,
|
icon: <IconFont type="ql-icon-script" />,
|
||||||
component: '@/pages/script/index',
|
component: '@/pages/script/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/dependence',
|
path: '/dependence',
|
||||||
name: '依赖管理',
|
name: '依赖管理',
|
||||||
icon: <ContainerOutlined />,
|
icon: <IconFont type="ql-icon-dependence" />,
|
||||||
component: '@/pages/dependence/index',
|
component: '@/pages/dependence/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/diff',
|
path: '/diff',
|
||||||
name: '对比工具',
|
name: '对比工具',
|
||||||
icon: <DiffOutlined />,
|
icon: <IconFont type="ql-icon-diff" />,
|
||||||
component: '@/pages/diff/index',
|
component: '@/pages/diff/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/log',
|
path: '/log',
|
||||||
name: '任务日志',
|
name: '任务日志',
|
||||||
icon: <FolderOutlined />,
|
icon: <IconFont type="ql-icon-log" />,
|
||||||
component: '@/pages/log/index',
|
component: '@/pages/log/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+32
-12
@@ -1,4 +1,4 @@
|
|||||||
@import '~antd/lib/style/themes/default.less';
|
@import '~antd/es/style/themes/default.less';
|
||||||
@import '~@/styles/variable.less';
|
@import '~@/styles/variable.less';
|
||||||
@import '~codemirror/lib/codemirror.css';
|
@import '~codemirror/lib/codemirror.css';
|
||||||
|
|
||||||
@@ -9,6 +9,12 @@
|
|||||||
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
|
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
max-height: calc(85vh - 110px);
|
||||||
|
max-height: calc(85vh - var(--vh-offset, 110px));
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.log-modal {
|
.log-modal {
|
||||||
.ant-modal {
|
.ant-modal {
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
@@ -26,6 +32,7 @@
|
|||||||
.monaco-editor:not(.rename-box) {
|
.monaco-editor:not(.rename-box) {
|
||||||
height: calc(100vh - 128px) !important;
|
height: calc(100vh - 128px) !important;
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;
|
height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;
|
||||||
|
|
||||||
.view-overlays .current-line {
|
.view-overlays .current-line {
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
@@ -33,6 +40,7 @@
|
|||||||
|
|
||||||
.rename-box {
|
.rename-box {
|
||||||
height: 0;
|
height: 0;
|
||||||
|
|
||||||
.rename-input {
|
.rename-input {
|
||||||
height: 0;
|
height: 0;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
@@ -43,10 +51,11 @@
|
|||||||
.ant-pro-grid-content.wide {
|
.ant-pro-grid-content.wide {
|
||||||
max-width: unset !important;
|
max-width: unset !important;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.ant-pro-page-container-children-content {
|
.ant-pro-page-container-children-content {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #fff;
|
background-color: @component-background;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,6 +66,7 @@
|
|||||||
|
|
||||||
.ant-tooltip {
|
.ant-tooltip {
|
||||||
max-width: 500px !important;
|
max-width: 500px !important;
|
||||||
|
|
||||||
.ant-tooltip-inner {
|
.ant-tooltip-inner {
|
||||||
word-break: break-all !important;
|
word-break: break-all !important;
|
||||||
}
|
}
|
||||||
@@ -72,6 +82,7 @@
|
|||||||
.log-select {
|
.log-select {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-page-header-heading-left {
|
.ant-page-header-heading-left {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
@@ -81,6 +92,7 @@
|
|||||||
.config-select {
|
.config-select {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-page-header-heading-left {
|
.ant-page-header-heading-left {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
@@ -119,10 +131,13 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100vh - 48px);
|
height: calc(100vh - 48px);
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 48px);
|
height: calc(100vh - var(--vh-offset, 0px) - 48px);
|
||||||
|
|
||||||
.ant-pro-grid-content.wide {
|
.ant-pro-grid-content.wide {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.ant-pro-grid-content-children {
|
.ant-pro-grid-content-children {
|
||||||
height: calc(100% - 36px);
|
height: calc(100% - 36px);
|
||||||
|
|
||||||
> div,
|
> div,
|
||||||
.log-container,
|
.log-container,
|
||||||
.react-codemirror2,
|
.react-codemirror2,
|
||||||
@@ -157,7 +172,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Resizer {
|
.Resizer {
|
||||||
background: #fff;
|
background: @component-background;
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
@@ -197,6 +212,7 @@
|
|||||||
.Resizer.disabled {
|
.Resizer.disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Resizer.disabled:hover {
|
.Resizer.disabled:hover {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
@@ -209,6 +225,7 @@
|
|||||||
|
|
||||||
.inline-countdown.ant-statistic {
|
.inline-countdown.ant-statistic {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
.ant-statistic-content {
|
.ant-statistic-content {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
@@ -253,6 +270,7 @@
|
|||||||
|
|
||||||
.ant-pro-basicLayout-content {
|
.ant-pro-basicLayout-content {
|
||||||
margin: 18px;
|
margin: 18px;
|
||||||
|
|
||||||
.ant-pro-page-container {
|
.ant-pro-page-container {
|
||||||
margin: -18px -18px -18px;
|
margin: -18px -18px -18px;
|
||||||
}
|
}
|
||||||
@@ -266,7 +284,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: @text-color;
|
||||||
|
|
||||||
.side-menu-collapse-button:hover {
|
.side-menu-collapse-button:hover {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
@@ -280,25 +298,27 @@
|
|||||||
|
|
||||||
.side-menu-user-drop-menu {
|
.side-menu-user-drop-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0;
|
|
||||||
padding: 4px 0;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background-clip: padding-box;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: #fff;
|
padding: 4px 0;
|
||||||
|
border: 1px solid fade(@component-background, 0.12);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%),
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
0 12px 48px 16px rgb(0 0 0 / 3%);
|
overflow: auto;
|
||||||
.ant-dropdown-menu-item:hover {
|
background-color: @component-background;
|
||||||
color: #1890ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-dark='true'] .side-menu-user-drop-menu {
|
||||||
|
background-color: #373739;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider-logo {
|
.ant-pro-sider-logo {
|
||||||
padding: 16px 8px !important;
|
padding: 16px 8px !important;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-left: 5px !important;
|
margin-left: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 32px !important;
|
width: 32px !important;
|
||||||
border-radius: 52% !important;
|
border-radius: 52% !important;
|
||||||
|
|||||||
+63
-60
@@ -1,11 +1,6 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
|
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
|
||||||
import {
|
import * as DarkReader from '@umijs/ssr-darkreader';
|
||||||
enable as enableDarkMode,
|
|
||||||
disable as disableDarkMode,
|
|
||||||
auto as followSystemColorScheme,
|
|
||||||
setFetchMethod,
|
|
||||||
} from 'darkreader';
|
|
||||||
import defaultProps from './defaultProps';
|
import defaultProps from './defaultProps';
|
||||||
import { Link, history } from 'umi';
|
import { Link, history } from 'umi';
|
||||||
import {
|
import {
|
||||||
@@ -24,24 +19,24 @@ import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import SockJS from 'sockjs-client';
|
import SockJS from 'sockjs-client';
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Integrations } from '@sentry/tracing';
|
import { init } from '../utils/init';
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
|
|
||||||
integrations: [new Integrations.BrowserTracing()],
|
|
||||||
release: version,
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function (props: any) {
|
export default function (props: any) {
|
||||||
const ctx = useCtx();
|
const ctx = useCtx();
|
||||||
const theme = useTheme();
|
const { theme, reloadTheme } = useTheme();
|
||||||
const [user, setUser] = useState<any>();
|
const [user, setUser] = useState<any>({});
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
|
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
|
||||||
const ws = useRef<any>(null);
|
const ws = useRef<any>(null);
|
||||||
const [socketMessage, setSocketMessage] = useState<any>();
|
const [socketMessage, setSocketMessage] = useState<any>();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const {
|
||||||
|
enable: enableDarkMode,
|
||||||
|
disable: disableDarkMode,
|
||||||
|
exportGeneratedCSS: collectCSS,
|
||||||
|
setFetchMethod,
|
||||||
|
auto: followSystemColorScheme,
|
||||||
|
} = DarkReader || {};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
request.post(`${config.apiPrefix}user/logout`).then(() => {
|
request.post(`${config.apiPrefix}user/logout`).then(() => {
|
||||||
@@ -91,26 +86,10 @@ export default function (props: any) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadUser = () => {
|
const reloadUser = (needLoading = false) => {
|
||||||
getUser(false);
|
getUser(needLoading);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTheme = () => {
|
|
||||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
const storageTheme = localStorage.getItem('qinglong_dark_theme');
|
|
||||||
const isDark =
|
|
||||||
(media.matches && storageTheme !== 'light') || storageTheme === 'dark';
|
|
||||||
if (isDark) {
|
|
||||||
document.body.setAttribute('data-dark', 'true');
|
|
||||||
} else {
|
|
||||||
document.body.setAttribute('data-dark', 'false');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
vhCheck();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (systemInfo && systemInfo.isInitialized && !user) {
|
if (systemInfo && systemInfo.isInitialized && !user) {
|
||||||
getUser();
|
getUser();
|
||||||
@@ -124,12 +103,25 @@ export default function (props: any) {
|
|||||||
}, [systemInfo]);
|
}, [systemInfo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTheme();
|
if (theme === 'vs-dark') {
|
||||||
}, [theme.theme]);
|
document.body.setAttribute('data-dark', 'true');
|
||||||
|
} else {
|
||||||
|
document.body.setAttribute('data-dark', 'false');
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
vhCheck();
|
||||||
|
init();
|
||||||
|
|
||||||
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||||
setFetchMethod(window.fetch);
|
if (typeof window === 'undefined') return;
|
||||||
|
if (typeof window.matchMedia === 'undefined') return;
|
||||||
|
if (!DarkReader) {
|
||||||
|
return () => null;
|
||||||
|
}
|
||||||
|
setFetchMethod(fetch);
|
||||||
|
|
||||||
if (_theme === 'dark') {
|
if (_theme === 'dark') {
|
||||||
enableDarkMode({});
|
enableDarkMode({});
|
||||||
} else if (_theme === 'light') {
|
} else if (_theme === 'light') {
|
||||||
@@ -137,6 +129,10 @@ export default function (props: any) {
|
|||||||
} else {
|
} else {
|
||||||
followSystemColorScheme({});
|
followSystemColorScheme({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disableDarkMode();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -187,7 +183,9 @@ export default function (props: any) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (['/login', '/initialization'].includes(props.location.pathname)) {
|
if (
|
||||||
|
['/login', '/initialization', '/error'].includes(props.location.pathname)
|
||||||
|
) {
|
||||||
document.title = `${
|
document.title = `${
|
||||||
(config.documentTitleMap as any)[props.location.pathname]
|
(config.documentTitleMap as any)[props.location.pathname]
|
||||||
} - 控制面板`;
|
} - 控制面板`;
|
||||||
@@ -198,14 +196,14 @@ export default function (props: any) {
|
|||||||
history.push('/crontab');
|
history.push('/crontab');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (systemInfo) {
|
if (systemInfo || props.location.pathname === '/error') {
|
||||||
return React.Children.map(props.children, (child) => {
|
return React.Children.map(props.children, (child) => {
|
||||||
return React.cloneElement(child, {
|
return React.cloneElement(child, {
|
||||||
...ctx,
|
...ctx,
|
||||||
...theme,
|
theme,
|
||||||
user,
|
user,
|
||||||
reloadUser,
|
reloadUser,
|
||||||
reloadTheme: setTheme,
|
reloadTheme,
|
||||||
ws: ws.current,
|
ws: ws.current,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -219,11 +217,11 @@ export default function (props: any) {
|
|||||||
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
|
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu className="side-menu-user-drop-menu">
|
<Menu
|
||||||
<Menu.Item key="1" icon={<LogoutOutlined />} onClick={logout}>
|
className="side-menu-user-drop-menu"
|
||||||
退出登录
|
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]}
|
||||||
</Menu.Item>
|
onClick={logout}
|
||||||
</Menu>
|
/>
|
||||||
);
|
);
|
||||||
return loading ? (
|
return loading ? (
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
@@ -232,12 +230,7 @@ export default function (props: any) {
|
|||||||
selectedKeys={[props.location.pathname]}
|
selectedKeys={[props.location.pathname]}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
ErrorBoundary={Sentry.ErrorBoundary}
|
ErrorBoundary={Sentry.ErrorBoundary}
|
||||||
logo={
|
logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />}
|
||||||
<Image
|
|
||||||
preview={false}
|
|
||||||
src="https://lf9-survey.bytetos.com/obj/web.business.image/202201205d0d7b5e576ee603497ab6f3"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<span style={{ fontSize: 16 }}>控制面板</span>
|
<span style={{ fontSize: 16 }}>控制面板</span>
|
||||||
@@ -283,10 +276,15 @@ export default function (props: any) {
|
|||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
rightContentRender={() =>
|
rightContentRender={() =>
|
||||||
ctx.isPhone && (
|
ctx.isPhone && (
|
||||||
<Dropdown overlay={menu} trigger={['click']}>
|
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
|
||||||
<span className="side-menu-user-wrapper">
|
<span className="side-menu-user-wrapper">
|
||||||
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
<Avatar
|
||||||
<span style={{ marginLeft: 5 }}>admin</span>
|
shape="square"
|
||||||
|
size="small"
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
src={`/api/static/${user.avatar}`}
|
||||||
|
/>
|
||||||
|
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||||
</span>
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)
|
)
|
||||||
@@ -300,10 +298,15 @@ export default function (props: any) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!collapsed && !ctx.isPhone && (
|
{!collapsed && !ctx.isPhone && (
|
||||||
<Dropdown overlay={menu} trigger={['hover']}>
|
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}>
|
||||||
<span className="side-menu-user-wrapper">
|
<span className="side-menu-user-wrapper">
|
||||||
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
<Avatar
|
||||||
<span style={{ marginLeft: 5 }}>admin</span>
|
shape="square"
|
||||||
|
size="small"
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
src={`/api/static/${user.avatar}`}
|
||||||
|
/>
|
||||||
|
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||||
</span>
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
@@ -320,10 +323,10 @@ export default function (props: any) {
|
|||||||
{React.Children.map(props.children, (child) => {
|
{React.Children.map(props.children, (child) => {
|
||||||
return React.cloneElement(child, {
|
return React.cloneElement(child, {
|
||||||
...ctx,
|
...ctx,
|
||||||
...theme,
|
theme,
|
||||||
user,
|
user,
|
||||||
reloadUser,
|
reloadUser,
|
||||||
reloadTheme: setTheme,
|
reloadTheme,
|
||||||
socketMessage,
|
socketMessage,
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,572 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Tag,
|
||||||
|
List,
|
||||||
|
Divider,
|
||||||
|
Typography,
|
||||||
|
Tooltip,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
ClockCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
FieldTimeOutlined,
|
||||||
|
Loading3QuartersOutlined,
|
||||||
|
FileOutlined,
|
||||||
|
PlayCircleOutlined,
|
||||||
|
PauseCircleOutlined,
|
||||||
|
FullscreenOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { CrontabStatus } from './index';
|
||||||
|
import { diffTime } from '@/utils/date';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import CronLogModal from './logModal';
|
||||||
|
import Editor from '@monaco-editor/react';
|
||||||
|
import IconFont from '@/components/iconfont';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const tabList = [
|
||||||
|
{
|
||||||
|
key: 'log',
|
||||||
|
tab: '日志',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'script',
|
||||||
|
tab: '脚本',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const LangMap: any = {
|
||||||
|
'.py': 'python',
|
||||||
|
'.js': 'javascript',
|
||||||
|
'.sh': 'shell',
|
||||||
|
'.ts': 'typescript',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LogItem {
|
||||||
|
directory: string;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = navigator.language || navigator.languages[0];
|
||||||
|
|
||||||
|
const CronDetailModal = ({
|
||||||
|
cron = {},
|
||||||
|
handleCancel,
|
||||||
|
visible,
|
||||||
|
theme,
|
||||||
|
isPhone,
|
||||||
|
}: {
|
||||||
|
cron?: any;
|
||||||
|
visible: boolean;
|
||||||
|
handleCancel: (needUpdate?: boolean) => void;
|
||||||
|
theme: string;
|
||||||
|
isPhone: boolean;
|
||||||
|
}) => {
|
||||||
|
const [activeTabKey, setActiveTabKey] = useState('log');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [logs, setLogs] = useState<LogItem[]>([]);
|
||||||
|
const [log, setLog] = useState('');
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const [scriptInfo, setScriptInfo] = useState<any>({});
|
||||||
|
const [logUrl, setLogUrl] = useState('');
|
||||||
|
const [validTabs, setValidTabs] = useState(tabList);
|
||||||
|
const [currentCron, setCurrentCron] = useState<any>({});
|
||||||
|
|
||||||
|
const contentList: any = {
|
||||||
|
log: (
|
||||||
|
<List
|
||||||
|
dataSource={logs}
|
||||||
|
loading={loading}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<List.Item className="log-item" onClick={() => onClickItem(item)}>
|
||||||
|
<FileOutlined style={{ marginRight: 10 }} />
|
||||||
|
{item.directory}/{item.filename}
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
script: scriptInfo.filename && (
|
||||||
|
<Editor
|
||||||
|
language={LangMap[scriptInfo.filename.slice(-3)] || ''}
|
||||||
|
theme={theme}
|
||||||
|
value={value}
|
||||||
|
options={{
|
||||||
|
fontSize: 12,
|
||||||
|
lineNumbersMinChars: 3,
|
||||||
|
fontFamily: 'Source Code Pro',
|
||||||
|
folding: false,
|
||||||
|
glyphMargin: false,
|
||||||
|
wordWrap: 'on',
|
||||||
|
}}
|
||||||
|
onMount={(editor, monaco) => {
|
||||||
|
console.log(monaco);
|
||||||
|
editorRef.current = editor;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickItem = (item: LogItem) => {
|
||||||
|
localStorage.setItem('logCron', currentCron.id);
|
||||||
|
setLogUrl(
|
||||||
|
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
|
||||||
|
);
|
||||||
|
request
|
||||||
|
.get(
|
||||||
|
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
|
||||||
|
)
|
||||||
|
.then((data) => {
|
||||||
|
setLog(data.data);
|
||||||
|
setIsLogModalVisible(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTabChange = (key: string) => {
|
||||||
|
setActiveTabKey(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogs = () => {
|
||||||
|
setLoading(true);
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}crons/${cron.id}/logs`)
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
setLogs(data.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScript = () => {
|
||||||
|
const cmd = cron.command.split(' ') as string[];
|
||||||
|
if (cmd[0] === 'task') {
|
||||||
|
setValidTabs(validTabs);
|
||||||
|
if (cmd[1].startsWith('/ql/data/scripts')) {
|
||||||
|
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
let [p, s] = cmd[1].split('/');
|
||||||
|
if (!s) {
|
||||||
|
s = p;
|
||||||
|
p = '';
|
||||||
|
}
|
||||||
|
setScriptInfo({ parent: p, filename: s });
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
|
||||||
|
.then((data) => {
|
||||||
|
setValue(data.data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setValidTabs([validTabs[0]]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveFile = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认保存`,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认保存文件
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{scriptInfo.filename}
|
||||||
|
</Text>{' '}
|
||||||
|
,保存后不可恢复
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
const content = editorRef.current
|
||||||
|
? editorRef.current.getValue().replace(/\r\n/g, '\n')
|
||||||
|
: value;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}scripts`, {
|
||||||
|
data: {
|
||||||
|
filename: scriptInfo.filename,
|
||||||
|
path: scriptInfo.parent || '',
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((_data: any) => {
|
||||||
|
if (_data.code === 200) {
|
||||||
|
setValue(content);
|
||||||
|
message.success(`保存成功`);
|
||||||
|
} else {
|
||||||
|
message.error(_data);
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
})
|
||||||
|
.catch((e) => reject(e));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const runCron = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认运行',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认运行定时任务{' '}
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{currentCron.name}
|
||||||
|
</Text>{' '}
|
||||||
|
吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
setCurrentCron({ ...currentCron, status: CrontabStatus.running });
|
||||||
|
setTimeout(() => {
|
||||||
|
getLogs();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopCron = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认停止',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认停止定时任务{' '}
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{currentCron.name}
|
||||||
|
</Text>{' '}
|
||||||
|
吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const enabledOrDisabledCron = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认${currentCron.isDisabled === 1 ? '启用' : '禁用'}`,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认{currentCron.isDisabled === 1 ? '启用' : '禁用'}
|
||||||
|
定时任务{' '}
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{currentCron.name}
|
||||||
|
</Text>{' '}
|
||||||
|
吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.put(
|
||||||
|
`${config.apiPrefix}crons/${
|
||||||
|
currentCron.isDisabled === 1 ? 'enable' : 'disable'
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
data: [currentCron.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
setCurrentCron({
|
||||||
|
...currentCron,
|
||||||
|
isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const pinOrUnPinCron = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认${currentCron.isPinned === 1 ? '取消置顶' : '置顶'}`,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认{currentCron.isPinned === 1 ? '取消置顶' : '置顶'}
|
||||||
|
定时任务{' '}
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{currentCron.name}
|
||||||
|
</Text>{' '}
|
||||||
|
吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.put(
|
||||||
|
`${config.apiPrefix}crons/${
|
||||||
|
currentCron.isPinned === 1 ? 'unpin' : 'pin'
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
data: [currentCron.id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
setCurrentCron({
|
||||||
|
...currentCron,
|
||||||
|
isPinned: currentCron.isPinned === 1 ? 0 : 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fullscreen = () => {
|
||||||
|
const editorElement = editorRef.current._domElement as HTMLElement;
|
||||||
|
editorElement.parentElement?.requestFullscreen();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cron && cron.id) {
|
||||||
|
setCurrentCron(cron);
|
||||||
|
getLogs();
|
||||||
|
getScript();
|
||||||
|
}
|
||||||
|
}, [cron]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div className="crontab-title-wrapper">
|
||||||
|
<div>
|
||||||
|
<span>{currentCron.name}</span>
|
||||||
|
{currentCron.labels?.length > 0 && currentCron.labels[0] !== '' && (
|
||||||
|
<Divider type="vertical"></Divider>
|
||||||
|
)}
|
||||||
|
{currentCron.labels?.length > 0 &&
|
||||||
|
currentCron.labels[0] !== '' &&
|
||||||
|
currentCron.labels?.map((label: string, i: number) => (
|
||||||
|
<Tag color="blue" style={{ marginRight: 5 }}>
|
||||||
|
{label}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="operations">
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
currentCron.status === CrontabStatus.idle ? '运行' : '停止'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={
|
||||||
|
currentCron.status === CrontabStatus.idle ? (
|
||||||
|
<PlayCircleOutlined />
|
||||||
|
) : (
|
||||||
|
<PauseCircleOutlined />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
onClick={
|
||||||
|
currentCron.status === CrontabStatus.idle ? runCron : stopCron
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={currentCron.isDisabled === 1 ? '启用' : '禁用'}>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={
|
||||||
|
<IconFont
|
||||||
|
type={
|
||||||
|
currentCron.isDisabled === 1
|
||||||
|
? 'ql-icon-enable'
|
||||||
|
: 'ql-icon-disable'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
onClick={enabledOrDisabledCron}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={currentCron.isPinned === 1 ? '取消置顶' : '置顶'}>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={
|
||||||
|
<IconFont
|
||||||
|
type={
|
||||||
|
currentCron.isPinned === 1
|
||||||
|
? 'ql-icon-untop'
|
||||||
|
: 'ql-icon-top'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
onClick={pinOrUnPinCron}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
centered
|
||||||
|
visible={visible}
|
||||||
|
forceRender
|
||||||
|
footer={false}
|
||||||
|
onCancel={() => handleCancel()}
|
||||||
|
wrapClassName="crontab-detail"
|
||||||
|
width={!isPhone ? '80vw' : ''}
|
||||||
|
>
|
||||||
|
<div className="card-wrapper">
|
||||||
|
<Card>
|
||||||
|
<div className="cron-detail-info-item">
|
||||||
|
<div className="cron-detail-info-title">任务</div>
|
||||||
|
<div className="cron-detail-info-value">{currentCron.command}</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ marginTop: 10 }}>
|
||||||
|
<div className="cron-detail-info-item">
|
||||||
|
<div className="cron-detail-info-title">状态</div>
|
||||||
|
<div className="cron-detail-info-value">
|
||||||
|
{(!currentCron.isDisabled ||
|
||||||
|
currentCron.status !== CrontabStatus.idle) && (
|
||||||
|
<>
|
||||||
|
{currentCron.status === CrontabStatus.idle && (
|
||||||
|
<Tag icon={<ClockCircleOutlined />} color="default">
|
||||||
|
空闲中
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{currentCron.status === CrontabStatus.running && (
|
||||||
|
<Tag
|
||||||
|
icon={<Loading3QuartersOutlined spin />}
|
||||||
|
color="processing"
|
||||||
|
>
|
||||||
|
运行中
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{currentCron.status === CrontabStatus.queued && (
|
||||||
|
<Tag icon={<FieldTimeOutlined />} color="default">
|
||||||
|
队列中
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{currentCron.isDisabled === 1 &&
|
||||||
|
currentCron.status === CrontabStatus.idle && (
|
||||||
|
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||||
|
已禁用
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cron-detail-info-item">
|
||||||
|
<div className="cron-detail-info-title">定时</div>
|
||||||
|
<div className="cron-detail-info-value">{currentCron.schedule}</div>
|
||||||
|
</div>
|
||||||
|
<div className="cron-detail-info-item">
|
||||||
|
<div className="cron-detail-info-title">最后运行时间</div>
|
||||||
|
<div className="cron-detail-info-value">
|
||||||
|
{currentCron.last_execution_time
|
||||||
|
? new Date(currentCron.last_execution_time * 1000)
|
||||||
|
.toLocaleString(language, {
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(' 24:', ' 00:')
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cron-detail-info-item">
|
||||||
|
<div className="cron-detail-info-title">最后运行时长</div>
|
||||||
|
<div className="cron-detail-info-value">
|
||||||
|
{currentCron.last_running_time
|
||||||
|
? diffTime(currentCron.last_running_time)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cron-detail-info-item">
|
||||||
|
<div className="cron-detail-info-title">下次运行时间</div>
|
||||||
|
<div className="cron-detail-info-value">
|
||||||
|
{currentCron.nextRunTime &&
|
||||||
|
currentCron.nextRunTime
|
||||||
|
.toLocaleString(language, {
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(' 24:', ' 00:')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
tabList={validTabs}
|
||||||
|
activeTabKey={activeTabKey}
|
||||||
|
onTabChange={(key) => {
|
||||||
|
onTabChange(key);
|
||||||
|
}}
|
||||||
|
tabBarExtraContent={
|
||||||
|
activeTabKey === 'script' && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
onClick={saveFile}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<FullscreenOutlined />}
|
||||||
|
onClick={fullscreen}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{contentList[activeTabKey]}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<CronLogModal
|
||||||
|
visible={isLogModalVisible}
|
||||||
|
handleCancel={() => {
|
||||||
|
setIsLogModalVisible(false);
|
||||||
|
}}
|
||||||
|
cron={cron}
|
||||||
|
data={log}
|
||||||
|
logUrl={logUrl}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CronDetailModal;
|
||||||
@@ -1,3 +1,103 @@
|
|||||||
.ant-table-pagination.ant-pagination {
|
.ant-table-pagination.ant-pagination {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cron {
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.crontab-detail {
|
||||||
|
.card-wrapper {
|
||||||
|
height: 80vh;
|
||||||
|
height: calc(80vh - var(--vh-offset, 0px));
|
||||||
|
|
||||||
|
.ant-card:last-child {
|
||||||
|
.ant-card-body {
|
||||||
|
height: calc(80vh - 238px);
|
||||||
|
height: calc(80vh - var(--vh-offset, 0px) - 238px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
background: #eee;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
.ant-card-head {
|
||||||
|
padding: 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card:first-child {
|
||||||
|
max-height: 66px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cron-detail-info-item {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.cron-detail-info-title {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cron-detail-info-value {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card:nth-child(2) {
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cron-detail-info-item {
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
.cron-detail-info-title {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cron-detail-info-value {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.crontab-title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-right: 32px;
|
||||||
|
|
||||||
|
.operations {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.ant-btn:not(:first-child) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+124
-105
@@ -33,16 +33,17 @@ import { PageContainer } from '@ant-design/pro-layout';
|
|||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import CronModal, { CronLabelModal } from './modal';
|
import CronModal, { CronLabelModal } from './modal';
|
||||||
import CronLogModal from './logModal';
|
import CronLogModal from './logModal';
|
||||||
|
import CronDetailModal from './detail';
|
||||||
import cron_parser from 'cron-parser';
|
import cron_parser from 'cron-parser';
|
||||||
import { diffTime } from '@/utils/date';
|
import { diffTime } from '@/utils/date';
|
||||||
import { getTableScroll } from '@/utils/index';
|
import { getTableScroll } from '@/utils/index';
|
||||||
import { history } from 'umi';
|
import { history } from 'umi';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
enum CrontabStatus {
|
export enum CrontabStatus {
|
||||||
'running',
|
'running',
|
||||||
'idle',
|
'idle',
|
||||||
'disabled',
|
'disabled',
|
||||||
@@ -69,10 +70,10 @@ enum OperationPath {
|
|||||||
'unpin',
|
'unpin',
|
||||||
}
|
}
|
||||||
|
|
||||||
const Crontab = ({ headerStyle, isPhone }: any) => {
|
const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||||
const columns: any = [
|
const columns: any = [
|
||||||
{
|
{
|
||||||
title: '任务名',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 150,
|
width: 150,
|
||||||
@@ -84,7 +85,32 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
goToScriptManager(record);
|
goToScriptManager(record);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{record.name || record._id}{' '}
|
{record.labels?.length > 0 && record.labels[0] !== '' ? (
|
||||||
|
<Popover
|
||||||
|
placement="right"
|
||||||
|
trigger={isPhone ? 'click' : 'hover'}
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
{record.labels?.map((label: string) => (
|
||||||
|
<Tag
|
||||||
|
color="blue"
|
||||||
|
style={{ cursor: 'point' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSearchValue(`label:${label}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a>{label}</a>
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{record.name || '-'}
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
record.name || '-'
|
||||||
|
)}
|
||||||
{record.isPinned ? (
|
{record.isPinned ? (
|
||||||
<span>
|
<span>
|
||||||
<PushpinOutlined />
|
<PushpinOutlined />
|
||||||
@@ -93,32 +119,6 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
''
|
''
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
<span>
|
|
||||||
{record.labels?.length > 0 && record.labels[0] !== '' ? (
|
|
||||||
<Popover
|
|
||||||
placement="right"
|
|
||||||
trigger={isPhone ? 'click' : 'hover'}
|
|
||||||
content={
|
|
||||||
<div>
|
|
||||||
{record.labels?.map((label: string, i: number) => (
|
|
||||||
<Tag
|
|
||||||
color="blue"
|
|
||||||
onClick={() => {
|
|
||||||
onSearch(`label:${label}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tag color="blue">{record.labels[0]}</Tag>
|
|
||||||
</Popover>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
sorter: {
|
sorter: {
|
||||||
@@ -127,23 +127,23 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '任务',
|
title: '命令',
|
||||||
dataIndex: 'command',
|
dataIndex: 'command',
|
||||||
key: 'command',
|
key: 'command',
|
||||||
width: 250,
|
width: 250,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<Paragraph
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'left',
|
|
||||||
width: '100%',
|
|
||||||
display: 'inline-block',
|
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-all',
|
||||||
|
marginBottom: 0,
|
||||||
|
textAlign: 'left',
|
||||||
}}
|
}}
|
||||||
|
ellipsis={{ tooltip: text, rows: 2 }}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</Paragraph>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
sorter: {
|
sorter: {
|
||||||
@@ -152,7 +152,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '任务定时',
|
title: '定时规则',
|
||||||
dataIndex: 'schedule',
|
dataIndex: 'schedule',
|
||||||
key: 'schedule',
|
key: 'schedule',
|
||||||
width: 110,
|
width: 110,
|
||||||
@@ -200,18 +200,9 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
return record.last_running_time
|
||||||
return (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{record.last_running_time
|
|
||||||
? diffTime(record.last_running_time)
|
? diffTime(record.last_running_time)
|
||||||
: '-'}
|
: '-';
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -225,19 +216,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
const language = navigator.language || navigator.languages[0];
|
||||||
return (
|
return record.nextRunTime
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{record.nextRunTime
|
|
||||||
.toLocaleString(language, {
|
.toLocaleString(language, {
|
||||||
hour12: false,
|
hour12: false,
|
||||||
})
|
})
|
||||||
.replace(' 24:', ' 00:')}
|
.replace(' 24:', ' 00:');
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -316,7 +299,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
{record.status === CrontabStatus.idle && (
|
{record.status === CrontabStatus.idle && (
|
||||||
<Tooltip title={isPc ? '运行' : ''}>
|
<Tooltip title={isPc ? '运行' : ''}>
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
runCron(record, index);
|
runCron(record, index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -327,7 +311,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
{record.status !== CrontabStatus.idle && (
|
{record.status !== CrontabStatus.idle && (
|
||||||
<Tooltip title={isPc ? '停止' : ''}>
|
<Tooltip title={isPc ? '停止' : ''}>
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
stopCron(record, index);
|
stopCron(record, index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -337,7 +322,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
)}
|
)}
|
||||||
<Tooltip title={isPc ? '日志' : ''}>
|
<Tooltip title={isPc ? '日志' : ''}>
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
setLogCron({ ...record, timestamp: Date.now() });
|
setLogCron({ ...record, timestamp: Date.now() });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -354,7 +340,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
const [value, setValue] = useState<any[]>([]);
|
const [value, setValue] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [isLabelModalVisible, setisLabelModalVisible] = useState(false);
|
const [isLabelModalVisible, setIsLabelModalVisible] = useState(false);
|
||||||
const [editedCron, setEditedCron] = useState();
|
const [editedCron, setEditedCron] = useState();
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||||
@@ -363,12 +349,15 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(20);
|
||||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||||
|
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
|
||||||
|
const [detailCron, setDetailCron] = useState<any>();
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
const goToScriptManager = (record: any) => {
|
const goToScriptManager = (record: any) => {
|
||||||
const cmd = record.command.split(' ') as string[];
|
const cmd = record.command.split(' ') as string[];
|
||||||
if (cmd[0] === 'task') {
|
if (cmd[0] === 'task') {
|
||||||
if (cmd[1].startsWith('/ql/scripts')) {
|
if (cmd[1].startsWith('/ql/data/scripts')) {
|
||||||
cmd[1] = cmd[1].replace('/ql/scripts/', '');
|
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
let [p, s] = cmd[1].split('/');
|
let [p, s] = cmd[1].split('/');
|
||||||
@@ -391,16 +380,16 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
data.data
|
data.data
|
||||||
.sort((a: any, b: any) => {
|
.sort((a: any, b: any) => {
|
||||||
const sortA =
|
const sortA =
|
||||||
a.isDisabled && a.status !== 0
|
a.isPinned && a.status !== 0
|
||||||
? 4
|
|
||||||
: a.isPinned && a.status !== 0
|
|
||||||
? 5
|
? 5
|
||||||
|
: a.isDisabled && a.status !== 0
|
||||||
|
? 4
|
||||||
: a.status;
|
: a.status;
|
||||||
const sortB =
|
const sortB =
|
||||||
b.isDisabled && b.status !== 0
|
b.isPinned && b.status !== 0
|
||||||
? 4
|
|
||||||
: b.isPinned && b.status !== 0
|
|
||||||
? 5
|
? 5
|
||||||
|
: b.isDisabled && b.status !== 0
|
||||||
|
? 4
|
||||||
: b.status;
|
: b.status;
|
||||||
return CrontabSort[sortA] - CrontabSort[sortB];
|
return CrontabSort[sortA] - CrontabSort[sortB];
|
||||||
})
|
})
|
||||||
@@ -449,8 +438,10 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
|
if (i !== -1) {
|
||||||
result.splice(i, 1);
|
result.splice(i, 1);
|
||||||
setValue(result);
|
setValue(result);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(data);
|
message.error(data);
|
||||||
}
|
}
|
||||||
@@ -481,11 +472,13 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
|
if (i !== -1) {
|
||||||
result.splice(i, 1, {
|
result.splice(i, 1, {
|
||||||
...record,
|
...record,
|
||||||
status: CrontabStatus.running,
|
status: CrontabStatus.running,
|
||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(data);
|
message.error(data);
|
||||||
}
|
}
|
||||||
@@ -516,12 +509,14 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
|
if (i !== -1) {
|
||||||
result.splice(i, 1, {
|
result.splice(i, 1, {
|
||||||
...record,
|
...record,
|
||||||
pid: null,
|
pid: null,
|
||||||
status: CrontabStatus.idle,
|
status: CrontabStatus.idle,
|
||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(data);
|
message.error(data);
|
||||||
}
|
}
|
||||||
@@ -561,11 +556,13 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
const newStatus = record.isDisabled === 1 ? 0 : 1;
|
const newStatus = record.isDisabled === 1 ? 0 : 1;
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
|
if (i !== -1) {
|
||||||
result.splice(i, 1, {
|
result.splice(i, 1, {
|
||||||
...record,
|
...record,
|
||||||
isDisabled: newStatus,
|
isDisabled: newStatus,
|
||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(data);
|
message.error(data);
|
||||||
}
|
}
|
||||||
@@ -605,11 +602,13 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
const newStatus = record.isPinned === 1 ? 0 : 1;
|
const newStatus = record.isPinned === 1 ? 0 : 1;
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
|
if (i !== -1) {
|
||||||
result.splice(i, 1, {
|
result.splice(i, 1, {
|
||||||
...record,
|
...record,
|
||||||
isPinned: newStatus,
|
isPinned: newStatus,
|
||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(data);
|
message.error(data);
|
||||||
}
|
}
|
||||||
@@ -621,45 +620,43 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getMenuItems = (record: any) => {
|
||||||
|
return [
|
||||||
|
{ label: '编辑', key: 'edit', icon: <EditOutlined /> },
|
||||||
|
{
|
||||||
|
label: record.isDisabled === 1 ? '启用' : '禁用',
|
||||||
|
key: 'enableOrDisable',
|
||||||
|
icon:
|
||||||
|
record.isDisabled === 1 ? <CheckCircleOutlined /> : <StopOutlined />,
|
||||||
|
},
|
||||||
|
{ label: '删除', key: 'delete', icon: <DeleteOutlined /> },
|
||||||
|
{
|
||||||
|
label: record.isPinned === 1 ? '取消置顶' : '置顶',
|
||||||
|
key: 'pinOrUnPin',
|
||||||
|
icon: record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
const MoreBtn: React.FC<{
|
const MoreBtn: React.FC<{
|
||||||
record: any;
|
record: any;
|
||||||
index: number;
|
index: number;
|
||||||
}> = ({ record, index }) => (
|
}> = ({ record, index }) => (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
arrow
|
arrow={{ pointAtCenter: true }}
|
||||||
|
placement="bottomRight"
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
overlay={
|
overlay={
|
||||||
<Menu onClick={({ key }) => action(key, record, index)}>
|
<Menu
|
||||||
<Menu.Item key="edit" icon={<EditOutlined />}>
|
items={getMenuItems(record)}
|
||||||
编辑
|
onClick={({ key, domEvent }) => {
|
||||||
</Menu.Item>
|
domEvent.stopPropagation();
|
||||||
<Menu.Item
|
action(key, record, index);
|
||||||
key="enableOrDisable"
|
}}
|
||||||
icon={
|
/>
|
||||||
record.isDisabled === 1 ? (
|
|
||||||
<CheckCircleOutlined />
|
|
||||||
) : (
|
|
||||||
<StopOutlined />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{record.isDisabled === 1 ? '启用' : '禁用'}
|
<a onClick={(e) => e.stopPropagation()}>
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
|
||||||
删除
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
|
||||||
key="pinOrUnPin"
|
|
||||||
icon={
|
|
||||||
record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{record.isPinned === 1 ? '取消置顶' : '置顶'}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<a>
|
|
||||||
<EllipsisOutlined />
|
<EllipsisOutlined />
|
||||||
</a>
|
</a>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -722,11 +719,13 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
.parseExpression(data.data.schedule)
|
.parseExpression(data.data.schedule)
|
||||||
.next()
|
.next()
|
||||||
.toDate();
|
.toDate();
|
||||||
|
if (index !== -1) {
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...cron,
|
...cron,
|
||||||
...data.data,
|
...data.data,
|
||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -804,7 +803,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getRowClassName = (record: any, index: number) => {
|
const getRowClassName = (record: any, index: number) => {
|
||||||
return record.isPinned ? 'pinned-cron' : '';
|
return record.isPinned ? 'pinned-cron cron' : 'cron';
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -834,7 +833,10 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
placeholder="请输入名称或者关键词"
|
placeholder="请输入名称或者关键词"
|
||||||
style={{ width: 'auto' }}
|
style={{ width: 'auto' }}
|
||||||
enterButton
|
enterButton
|
||||||
|
allowClear
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
/>,
|
/>,
|
||||||
<Button key="2" type="primary" onClick={() => addCron()}>
|
<Button key="2" type="primary" onClick={() => addCron()}>
|
||||||
@@ -890,7 +892,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => setisLabelModalVisible(true)}
|
onClick={() => setIsLabelModalVisible(true)}
|
||||||
style={{ marginLeft: 8, marginRight: 8 }}
|
style={{ marginLeft: 8, marginRight: 8 }}
|
||||||
>
|
>
|
||||||
批量修改标签
|
批量修改标签
|
||||||
@@ -912,7 +914,15 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
defaultPageSize: 20,
|
defaultPageSize: 20,
|
||||||
showTotal: (total: number, range: number[]) =>
|
showTotal: (total: number, range: number[]) =>
|
||||||
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
||||||
pageSizeOptions: [10, 20, 50, 100, 200, 500, 1000],
|
pageSizeOptions: [20, 100, 500, 1000] as any,
|
||||||
|
}}
|
||||||
|
onRow={(record) => {
|
||||||
|
return {
|
||||||
|
onClick: (event) => {
|
||||||
|
setDetailCron(record);
|
||||||
|
setIsDetailModalVisible(true);
|
||||||
|
},
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
dataSource={value}
|
dataSource={value}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
@@ -938,13 +948,22 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
|||||||
<CronLabelModal
|
<CronLabelModal
|
||||||
visible={isLabelModalVisible}
|
visible={isLabelModalVisible}
|
||||||
handleCancel={(needUpdate?: boolean) => {
|
handleCancel={(needUpdate?: boolean) => {
|
||||||
setisLabelModalVisible(false);
|
setIsLabelModalVisible(false);
|
||||||
if (needUpdate) {
|
if (needUpdate) {
|
||||||
getCrons();
|
getCrons();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ids={selectedRowIds}
|
ids={selectedRowIds}
|
||||||
/>
|
/>
|
||||||
|
<CronDetailModal
|
||||||
|
visible={isDetailModalVisible}
|
||||||
|
handleCancel={() => {
|
||||||
|
setIsDetailModalVisible(false);
|
||||||
|
}}
|
||||||
|
cron={detailCron}
|
||||||
|
theme={theme}
|
||||||
|
isPhone={isPhone}
|
||||||
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ const CronLogModal = ({
|
|||||||
cron,
|
cron,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
visible,
|
visible,
|
||||||
|
data,
|
||||||
|
logUrl,
|
||||||
}: {
|
}: {
|
||||||
cron?: any;
|
cron?: any;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
|
data?: string;
|
||||||
|
logUrl?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [value, setValue] = useState<string>('启动中...');
|
const [value, setValue] = useState<string>('启动中...');
|
||||||
const [loading, setLoading] = useState<any>(true);
|
const [loading, setLoading] = useState<any>(true);
|
||||||
@@ -36,7 +40,7 @@ const CronLogModal = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
}
|
}
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}crons/${cron.id}/log`)
|
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (localStorage.getItem('logCron') === String(cron.id)) {
|
if (localStorage.getItem('logCron') === String(cron.id)) {
|
||||||
const log = data.data as string;
|
const log = data.data as string;
|
||||||
@@ -91,16 +95,22 @@ const CronLogModal = ({
|
|||||||
<>
|
<>
|
||||||
{(executing || loading) && <Loading3QuartersOutlined spin />}
|
{(executing || loading) && <Loading3QuartersOutlined spin />}
|
||||||
{!executing && !loading && <CheckCircleOutlined />}
|
{!executing && !loading && <CheckCircleOutlined />}
|
||||||
<span style={{ marginLeft: 5 }}>日志-{cron && cron.name}</span>{' '}
|
<span style={{ marginLeft: 5 }}>{cron && cron.name}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cron) {
|
if (cron && cron.id && visible) {
|
||||||
getCronLog(true);
|
getCronLog(true);
|
||||||
}
|
}
|
||||||
}, [cron]);
|
}, [cron, visible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setValue(data);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsPhone(document.body.clientWidth < 768);
|
setIsPhone(document.body.clientWidth < 768);
|
||||||
@@ -113,8 +123,6 @@ const CronLogModal = ({
|
|||||||
centered
|
centered
|
||||||
className="log-modal"
|
className="log-modal"
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
overflowY: 'auto',
|
|
||||||
maxHeight: 'calc(80vh - var(--vh-offset, 0px))',
|
|
||||||
minHeight: '300px',
|
minHeight: '300px',
|
||||||
}}
|
}}
|
||||||
forceRender
|
forceRender
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Modal, message, Input, Form, Button } from 'antd';
|
|||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import cronParse from 'cron-parser';
|
import cronParse from 'cron-parser';
|
||||||
|
import EditableTagGroup from '@/components/tag';
|
||||||
|
|
||||||
const CronModal = ({
|
const CronModal = ({
|
||||||
cron,
|
cron,
|
||||||
@@ -20,11 +21,6 @@ const CronModal = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const method = cron ? 'put' : 'post';
|
const method = cron ? 'put' : 'post';
|
||||||
const payload = { ...values };
|
const payload = { ...values };
|
||||||
if (typeof payload.labels === 'string') {
|
|
||||||
payload.labels = values.labels.split(/,|,/);
|
|
||||||
} else if (!payload.labels) {
|
|
||||||
payload.labels = [];
|
|
||||||
}
|
|
||||||
if (cron) {
|
if (cron) {
|
||||||
payload.id = cron.id;
|
payload.id = cron.id;
|
||||||
}
|
}
|
||||||
@@ -53,6 +49,8 @@ const CronModal = ({
|
|||||||
title={cron ? '编辑任务' : '新建任务'}
|
title={cron ? '编辑任务' : '新建任务'}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
forceRender
|
forceRender
|
||||||
|
centered
|
||||||
|
maskClosable={false}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
form
|
form
|
||||||
.validateFields()
|
.validateFields()
|
||||||
@@ -93,7 +91,7 @@ const CronModal = ({
|
|||||||
{ required: true },
|
{ required: true },
|
||||||
{
|
{
|
||||||
validator: (rule, value) => {
|
validator: (rule, value) => {
|
||||||
if (cronParse.parseExpression(value).hasNext()) {
|
if (!value || cronParse.parseExpression(value).hasNext()) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject('Cron表达式格式有误');
|
return Promise.reject('Cron表达式格式有误');
|
||||||
@@ -105,7 +103,7 @@ const CronModal = ({
|
|||||||
<Input placeholder="秒(可选) 分 时 天 月 周" />
|
<Input placeholder="秒(可选) 分 时 天 月 周" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="labels" label="标签">
|
<Form.Item name="labels" label="标签">
|
||||||
<Input placeholder="请输入任务标签" />
|
<EditableTagGroup />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -128,9 +126,6 @@ const CronLabelModal = ({
|
|||||||
form
|
form
|
||||||
.validateFields()
|
.validateFields()
|
||||||
.then(async (values) => {
|
.then(async (values) => {
|
||||||
if (typeof values.labels === 'string') {
|
|
||||||
values.labels = values.labels.split(/,|,/);
|
|
||||||
}
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const payload = { ids, labels: values.labels };
|
const payload = { ids, labels: values.labels };
|
||||||
const { code, data } = await request[action](
|
const { code, data } = await request[action](
|
||||||
@@ -173,13 +168,15 @@ const CronLabelModal = ({
|
|||||||
title="批量修改标签"
|
title="批量修改标签"
|
||||||
visible={visible}
|
visible={visible}
|
||||||
footer={buttons}
|
footer={buttons}
|
||||||
|
centered
|
||||||
|
maskClosable={false}
|
||||||
forceRender
|
forceRender
|
||||||
onCancel={() => handleCancel(false)}
|
onCancel={() => handleCancel(false)}
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" name="form_in_label_modal">
|
<Form form={form} layout="vertical" name="form_in_label_modal">
|
||||||
<Form.Item name="labels" label="标签">
|
<Form.Item name="labels" label="标签">
|
||||||
<Input placeholder="请输入任务标签" />
|
<EditableTagGroup />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
StopOutlined,
|
DeleteFilled,
|
||||||
BugOutlined,
|
BugOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
@@ -120,6 +120,15 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
const isPc = !isPhone;
|
const isPc = !isPhone;
|
||||||
return (
|
return (
|
||||||
<Space size="middle">
|
<Space size="middle">
|
||||||
|
<Tooltip title={isPc ? '日志' : ''}>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
setLogDependence({ ...record, timestamp: Date.now() });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileTextOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
{record.status !== Status.安装中 &&
|
{record.status !== Status.安装中 &&
|
||||||
record.status !== Status.删除中 && (
|
record.status !== Status.删除中 && (
|
||||||
<>
|
<>
|
||||||
@@ -133,17 +142,13 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
<Tooltip title={isPc ? '强制删除' : ''}>
|
||||||
)}
|
<a onClick={() => deleteDependence(record, index, true)}>
|
||||||
<Tooltip title={isPc ? '日志' : ''}>
|
<DeleteFilled />
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
setLogDependence({ ...record, timestamp: Date.now() });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FileTextOutlined />
|
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -182,7 +187,11 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
setIsModalVisible(true);
|
setIsModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteDependence = (record: any, index: number) => {
|
const deleteDependence = (
|
||||||
|
record: any,
|
||||||
|
index: number,
|
||||||
|
force: boolean = false,
|
||||||
|
) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: (
|
content: (
|
||||||
@@ -196,10 +205,19 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
),
|
),
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.delete(`${config.apiPrefix}dependencies`, { data: [record.id] })
|
.delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {
|
||||||
|
data: [record.id],
|
||||||
|
})
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
handleDependence(data.data[0]);
|
if (force) {
|
||||||
|
const i = value.findIndex((x) => x.id === data.data[0].id);
|
||||||
|
if (i !== -1) {
|
||||||
|
const result = [...value];
|
||||||
|
result.splice(i, 1);
|
||||||
|
setValue(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(data);
|
message.error(data);
|
||||||
}
|
}
|
||||||
@@ -253,10 +271,12 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
result.push(...dependence);
|
result.push(...dependence);
|
||||||
} else {
|
} else {
|
||||||
const index = value.findIndex((x) => x.id === dependence.id);
|
const index = value.findIndex((x) => x.id === dependence.id);
|
||||||
|
if (index !== -1) {
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...dependence,
|
...dependence,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
setValue(result);
|
setValue(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,13 +295,16 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
onChange: onSelectChange,
|
onChange: onSelectChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const delDependencies = () => {
|
const delDependencies = (force: boolean) => {
|
||||||
|
const forceUrl = force ? '/force' : '';
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: <>确认删除选中的依赖吗</>,
|
content: <>确认删除选中的依赖吗</>,
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.delete(`${config.apiPrefix}dependencies`, { data: selectedRowIds })
|
.delete(`${config.apiPrefix}dependencies${forceUrl}`, {
|
||||||
|
data: selectedRowIds,
|
||||||
|
})
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
setSelectedRowIds([]);
|
setSelectedRowIds([]);
|
||||||
@@ -303,11 +326,13 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
const index = value.findIndex((x) => x.id === dependence.id);
|
const index = value.findIndex((x) => x.id === dependence.id);
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
|
if (index !== -1) {
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...dependence,
|
...dependence,
|
||||||
...data.data,
|
...data.data,
|
||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -350,11 +375,13 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
const result = [...value];
|
const result = [...value];
|
||||||
for (let i = 0; i < references.length; i++) {
|
for (let i = 0; i < references.length; i++) {
|
||||||
const index = value.findIndex((x) => x.id === references[i]);
|
const index = value.findIndex((x) => x.id === references[i]);
|
||||||
|
if (index !== -1) {
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...result[index],
|
...value[index],
|
||||||
status,
|
status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
setValue(result);
|
setValue(result);
|
||||||
|
|
||||||
if (status === Status.已删除) {
|
if (status === Status.已删除) {
|
||||||
@@ -362,8 +389,10 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
const _result = [...value];
|
const _result = [...value];
|
||||||
for (let i = 0; i < references.length; i++) {
|
for (let i = 0; i < references.length; i++) {
|
||||||
const index = value.findIndex((x) => x.id === references[i]);
|
const index = value.findIndex((x) => x.id === references[i]);
|
||||||
|
if (index !== -1) {
|
||||||
_result.splice(index, 1);
|
_result.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
setValue(_result);
|
setValue(_result);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
@@ -377,10 +406,17 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
style={{ marginBottom: 5, marginLeft: 8 }}
|
style={{ marginBottom: 5, marginLeft: 8 }}
|
||||||
onClick={delDependencies}
|
onClick={() => delDependencies(false)}
|
||||||
>
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginBottom: 5, marginLeft: 8 }}
|
||||||
|
onClick={() => delDependencies(true)}
|
||||||
|
>
|
||||||
|
批量强制删除
|
||||||
|
</Button>
|
||||||
<span style={{ marginLeft: 8 }}>
|
<span style={{ marginLeft: 8 }}>
|
||||||
已选择
|
已选择
|
||||||
<a>{selectedRowIds?.length}</a>项
|
<a>{selectedRowIds?.length}</a>项
|
||||||
@@ -455,8 +491,10 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
if (needRemove) {
|
if (needRemove) {
|
||||||
const index = value.findIndex((x) => x.id === logDependence.id);
|
const index = value.findIndex((x) => x.id === logDependence.id);
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
|
if (index !== -1) {
|
||||||
result.splice(index, 1);
|
result.splice(index, 1);
|
||||||
setValue(result);
|
setValue(result);
|
||||||
|
}
|
||||||
} else if ([...value].map((x) => x.id).includes(logDependence.id)) {
|
} else if ([...value].map((x) => x.id).includes(logDependence.id)) {
|
||||||
getDependenceDetail(logDependence);
|
getDependenceDetail(logDependence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { PageLoading } from '@ant-design/pro-layout';
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
|
import Ansi from 'ansi-to-react';
|
||||||
|
|
||||||
const DependenceLogModal = ({
|
const DependenceLogModal = ({
|
||||||
dependence,
|
dependence,
|
||||||
@@ -89,17 +90,19 @@ const DependenceLogModal = ({
|
|||||||
}, [dependence]);
|
}, [dependence]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socketMessage) return;
|
if (!socketMessage || !dependence) return;
|
||||||
const { type, message, references } = socketMessage;
|
const { type, message, references } = socketMessage;
|
||||||
if (
|
if (
|
||||||
type === 'installDependence' &&
|
type === 'installDependence' &&
|
||||||
message.includes('结束时间') &&
|
references.length > 0 &&
|
||||||
references.length > 0
|
references.includes(dependence.id)
|
||||||
) {
|
) {
|
||||||
|
if (message.includes('结束时间')) {
|
||||||
setExecuting(false);
|
setExecuting(false);
|
||||||
setIsRemoveFailed(message.includes('删除失败'));
|
setIsRemoveFailed(message.includes('删除失败'));
|
||||||
}
|
}
|
||||||
setValue(`${value}${message}`);
|
setValue(`${value}${message}`);
|
||||||
|
}
|
||||||
}, [socketMessage]);
|
}, [socketMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -140,7 +143,7 @@ const DependenceLogModal = ({
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{value}
|
<Ansi>{value}</Ansi>
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ const DependenceModal = ({
|
|||||||
title={dependence ? '编辑依赖' : '新建依赖'}
|
title={dependence ? '编辑依赖' : '新建依赖'}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
forceRender
|
forceRender
|
||||||
|
centered
|
||||||
|
maskClosable={false}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
form
|
form
|
||||||
.validateFields()
|
.validateFields()
|
||||||
|
|||||||
Vendored
+2
@@ -41,6 +41,8 @@ const EditNameModal = ({
|
|||||||
title="修改环境变量名称"
|
title="修改环境变量名称"
|
||||||
visible={visible}
|
visible={visible}
|
||||||
forceRender
|
forceRender
|
||||||
|
centered
|
||||||
|
maskClosable={false}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
form
|
form
|
||||||
.validateFields()
|
.validateFields()
|
||||||
|
|||||||
Vendored
+32
-11
@@ -25,9 +25,9 @@ import EditNameModal from './editNameModal';
|
|||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import { getTableScroll } from '@/utils/index';
|
import { exportJson, getTableScroll } from '@/utils/index';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
enum Status {
|
enum Status {
|
||||||
@@ -119,18 +119,18 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
key: 'value',
|
key: 'value',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
width: '35%',
|
width: '35%',
|
||||||
ellipsis: {
|
|
||||||
showTitle: false,
|
|
||||||
},
|
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Paragraph
|
||||||
placement="topLeft"
|
style={{
|
||||||
title={text}
|
wordBreak: 'break-all',
|
||||||
trigger={['hover', 'click']}
|
marginBottom: 0,
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
ellipsis={{ tooltip: text, rows: 2 }}
|
||||||
>
|
>
|
||||||
<span>{text}</span>
|
{text}
|
||||||
</Tooltip>
|
</Paragraph>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -149,6 +149,13 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false,
|
showTitle: false,
|
||||||
},
|
},
|
||||||
|
sorter: {
|
||||||
|
compare: (a: any, b: any) => {
|
||||||
|
const updatedAtA = new Date(a.updatedAt || a.timestamp).getTime();
|
||||||
|
const updatedAtB = new Date(b.updatedAt || b.timestamp).getTime();
|
||||||
|
return updatedAtA - updatedAtB;
|
||||||
|
},
|
||||||
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
const language = navigator.language || navigator.languages[0];
|
||||||
const time = record.updatedAt || record.timestamp;
|
const time = record.updatedAt || record.timestamp;
|
||||||
@@ -455,6 +462,13 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportEnvs = () => {
|
||||||
|
const envs = value
|
||||||
|
.filter((x) => selectedRowIds.includes(x.id))
|
||||||
|
.map((x) => ({ value: x.value, name: x.name, remarks: x.remarks }));
|
||||||
|
exportJson('env.json', JSON.stringify(envs));
|
||||||
|
};
|
||||||
|
|
||||||
const modifyName = () => {
|
const modifyName = () => {
|
||||||
setIsEditNameModalVisible(true);
|
setIsEditNameModalVisible(true);
|
||||||
};
|
};
|
||||||
@@ -509,6 +523,13 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
>
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => exportEnvs()}
|
||||||
|
style={{ marginLeft: 8, marginRight: 8 }}
|
||||||
|
>
|
||||||
|
批量导出
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => operateEnvs(0)}
|
onClick={() => operateEnvs(0)}
|
||||||
|
|||||||
Vendored
+2
@@ -61,6 +61,8 @@ const EnvModal = ({
|
|||||||
title={env ? '编辑变量' : '新建变量'}
|
title={env ? '编辑变量' : '新建变量'}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
forceRender
|
forceRender
|
||||||
|
centered
|
||||||
|
maskClosable={false}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
form
|
form
|
||||||
.validateFields()
|
.validateFields()
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.error-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
.react-terminal-wrapper {
|
||||||
|
max-width: 90%;
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import Terminal, { ColorMode, LineType } from '../../components/terminal';
|
||||||
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
|
import { history } from 'umi';
|
||||||
|
import Ansi from 'ansi-to-react';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const Error = ({ user, theme }: any) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState('暂无日志');
|
||||||
|
|
||||||
|
const getLog = () => {
|
||||||
|
setLoading(true);
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}public/panel/log`)
|
||||||
|
.then((data: any) => {
|
||||||
|
setData(data.data);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && user.username) {
|
||||||
|
history.push('/crontab');
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLog();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="error-wrapper">
|
||||||
|
{loading ? (
|
||||||
|
<PageLoading />
|
||||||
|
) : (
|
||||||
|
<Terminal
|
||||||
|
name="服务错误"
|
||||||
|
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
|
||||||
|
lineData={[
|
||||||
|
{ type: LineType.Input, value: 'pm2 logs panel' },
|
||||||
|
{
|
||||||
|
type: LineType.Output,
|
||||||
|
value: (
|
||||||
|
<pre>
|
||||||
|
<Ansi>{data}</Ansi>
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Error;
|
||||||
@@ -233,7 +233,7 @@ const Initialization = () => {
|
|||||||
<img
|
<img
|
||||||
alt="logo"
|
alt="logo"
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
src="https://lf9-survey.bytetos.com/obj/web.business.image/202201205d0d7b5e576ee603497ab6f3"
|
src="http://qn.whyour.cn/logo.png"
|
||||||
/>
|
/>
|
||||||
<span className={styles.title}>初始化配置</span>
|
<span className={styles.title}>初始化配置</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
@import '~antd/es/style/themes/default.less';
|
||||||
@import '~@/styles/variable.less';
|
@import '~@/styles/variable.less';
|
||||||
|
|
||||||
.left-tree {
|
.left-tree {
|
||||||
&-container {
|
&-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #fff;
|
background-color: @component-background;
|
||||||
height: calc(100vh - 128px);
|
height: calc(100vh - 128px);
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -13,7 +14,6 @@
|
|||||||
&-scroller {
|
&-scroller {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-right: 1px dashed #f0f0f0;
|
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,3 +22,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.Pane.vertical.Pane1 {
|
||||||
|
padding-right: 5px;
|
||||||
|
border-right: 1px dashed @text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+7
-22
@@ -52,32 +52,16 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}logs`)
|
.get(`${config.apiPrefix}logs`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const result = formatData(data.dirs) as any;
|
setData(data.data);
|
||||||
setData(result);
|
setFilterData(data.data);
|
||||||
setFilterData(result);
|
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatData = (tree: any[]) => {
|
|
||||||
return tree.map((x) => {
|
|
||||||
x.title = x.name;
|
|
||||||
x.value = x.name;
|
|
||||||
x.disabled = x.isDir;
|
|
||||||
x.key = x.name;
|
|
||||||
x.children = x.files.map((y: string) => ({
|
|
||||||
title: y,
|
|
||||||
value: `${x.name}/${y}`,
|
|
||||||
key: `${x.name}/${y}`,
|
|
||||||
parent: x.name,
|
|
||||||
isLeaf: true,
|
|
||||||
}));
|
|
||||||
return x;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLog = (node: any) => {
|
const getLog = (node: any) => {
|
||||||
request.get(`${config.apiPrefix}logs/${node.value}`).then((data) => {
|
request
|
||||||
|
.get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
|
||||||
|
.then((data) => {
|
||||||
setValue(data.data);
|
setValue(data.data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -88,7 +72,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
}
|
}
|
||||||
setValue('加载中...');
|
setValue('加载中...');
|
||||||
setSelect(value);
|
setSelect(value);
|
||||||
setTitle(node.parent || node.value);
|
setTitle(node.key);
|
||||||
getLog(node);
|
getLog(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,6 +113,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||||
treeData={data}
|
treeData={data}
|
||||||
placeholder="请选择日志"
|
placeholder="请选择日志"
|
||||||
|
fieldNames={{ value: 'key', label: 'title' }}
|
||||||
showSearch
|
showSearch
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { MobileOutlined } from '@ant-design/icons';
|
|||||||
const FormItem = Form.Item;
|
const FormItem = Form.Item;
|
||||||
const { Countdown } = Statistic;
|
const { Countdown } = Statistic;
|
||||||
|
|
||||||
const Login = () => {
|
const Login = ({ reloadUser }: any) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [waitTime, setWaitTime] = useState<any>();
|
const [waitTime, setWaitTime] = useState<any>();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -100,8 +100,8 @@ const Login = () => {
|
|||||||
<div>上次登录状态:{retries > 0 ? `失败${retries}次` : '成功'}</div>
|
<div>上次登录状态:{retries > 0 ? `失败${retries}次` : '成功'}</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
duration: 5,
|
|
||||||
});
|
});
|
||||||
|
reloadUser(true);
|
||||||
history.push('/crontab');
|
history.push('/crontab');
|
||||||
} else if (data.code === 100) {
|
} else if (data.code === 100) {
|
||||||
message.warn(data.message);
|
message.warn(data.message);
|
||||||
@@ -135,7 +135,7 @@ const Login = () => {
|
|||||||
<img
|
<img
|
||||||
alt="logo"
|
alt="logo"
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
src="https://lf9-survey.bytetos.com/obj/web.business.image/202201205d0d7b5e576ee603497ab6f3"
|
src="http://qn.whyour.cn/logo.png"
|
||||||
/>
|
/>
|
||||||
<span className={styles.title}>
|
<span className={styles.title}>
|
||||||
{twoFactor ? '两步验证' : config.siteName}
|
{twoFactor ? '两步验证' : config.siteName}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const EditModal = ({
|
|||||||
|
|
||||||
const getDetail = (node: any) => {
|
const getDetail = (node: any) => {
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
|
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setValue(data.data);
|
setValue(data.data);
|
||||||
});
|
});
|
||||||
@@ -74,11 +74,13 @@ const EditModal = ({
|
|||||||
|
|
||||||
const run = () => {
|
const run = () => {
|
||||||
setLog('');
|
setLog('');
|
||||||
|
const content = editorRef.current.getValue().replace(/\r\n/g, '\n');
|
||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}scripts/run`, {
|
.put(`${config.apiPrefix}scripts/run`, {
|
||||||
data: {
|
data: {
|
||||||
filename: cNode.value,
|
filename: cNode.title,
|
||||||
path: cNode.parent || '',
|
path: cNode.parent || '',
|
||||||
|
content,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -87,14 +89,16 @@ const EditModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
if (!cNode || !cNode.value) {
|
if (!cNode || !cNode.title) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const content = editorRef.current.getValue().replace(/\r\n/g, '\n');
|
||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}scripts/stop`, {
|
.put(`${config.apiPrefix}scripts/stop`, {
|
||||||
data: {
|
data: {
|
||||||
filename: cNode.value,
|
filename: cNode.title,
|
||||||
path: cNode.parent || '',
|
path: cNode.parent || '',
|
||||||
|
content,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -113,8 +117,7 @@ const EditModal = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_message === '执行结束') {
|
if (_message.includes('执行结束')) {
|
||||||
_message = '';
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
}, 300);
|
}, 300);
|
||||||
@@ -146,6 +149,7 @@ const EditModal = ({
|
|||||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
placeholder="请选择脚本文件"
|
placeholder="请选择脚本文件"
|
||||||
|
fieldNames={{ value: 'key', label: 'title' }}
|
||||||
showSearch
|
showSearch
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>
|
/>
|
||||||
@@ -243,7 +247,7 @@ const EditModal = ({
|
|||||||
content:
|
content:
|
||||||
editorRef.current &&
|
editorRef.current &&
|
||||||
editorRef.current.getValue().replace(/\r\n/g, '\n'),
|
editorRef.current.getValue().replace(/\r\n/g, '\n'),
|
||||||
filename: cNode?.value,
|
filename: cNode?.title,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SettingModal
|
<SettingModal
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Modal, message, Input, Form, Select } from 'antd';
|
import {
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
Select,
|
||||||
|
Upload,
|
||||||
|
Radio,
|
||||||
|
TreeSelect,
|
||||||
|
} from 'antd';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
@@ -21,21 +31,30 @@ const EditScriptNameModal = ({
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [dirs, setDirs] = useState<any[]>([]);
|
const [dirs, setDirs] = useState<any[]>([]);
|
||||||
|
const [file, setFile] = useState<File>();
|
||||||
|
const [type, setType] = useState<'blank' | 'upload'>('blank');
|
||||||
|
|
||||||
const handleOk = async (values: any) => {
|
const handleOk = async (values: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
values.path = values.path || '';
|
values.path = values.path || '';
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file as any);
|
||||||
|
formData.append('filename', values.filename);
|
||||||
|
formData.append('path', values.path);
|
||||||
|
formData.append('content', '');
|
||||||
request
|
request
|
||||||
.post(`${config.apiPrefix}scripts`, {
|
.post(`${config.apiPrefix}scripts`, {
|
||||||
data: { filename: values.filename, path: values.path, content: '' },
|
data: formData,
|
||||||
})
|
})
|
||||||
.then(({ code, data }) => {
|
.then(({ code, data }) => {
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
message.success('保存文件成功');
|
message.success('新建文件成功');
|
||||||
|
const key = values.path ? `${values.path}/` : '';
|
||||||
|
const filename = file ? file.name : values.filename;
|
||||||
handleCancel({
|
handleCancel({
|
||||||
filename: values.filename,
|
filename,
|
||||||
path: values.path,
|
path: values.path,
|
||||||
key: `${values.path}-${values.filename}`,
|
key: `${key}${filename}`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
message.error(data);
|
message.error(data);
|
||||||
@@ -45,17 +64,46 @@ const EditScriptNameModal = ({
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const beforeUpload = (file: File) => {
|
||||||
|
setFile(file);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeChange = (e) => {
|
||||||
|
setType(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDirs = (data) => {
|
||||||
|
for (const item of data) {
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
item.children = item.children
|
||||||
|
.filter((x) => x.type === 'directory')
|
||||||
|
.map((x) => ({ ...x, disabled: false }));
|
||||||
|
getDirs(item.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const originDirs = treeData
|
||||||
|
.filter((x) => x.type === 'directory')
|
||||||
|
.map((x) => ({ ...x, disabled: false }));
|
||||||
|
const dirs = getDirs(originDirs);
|
||||||
|
setDirs(dirs);
|
||||||
|
}, [treeData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
const originDirs = treeData.filter((x) => x.disabled);
|
|
||||||
setDirs([{ key: '' }, ...originDirs]);
|
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="新建文件"
|
title="新建脚本"
|
||||||
visible={visible}
|
visible={visible}
|
||||||
forceRender
|
forceRender
|
||||||
|
centered
|
||||||
|
maskClosable={false}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
form
|
form
|
||||||
.validateFields()
|
.validateFields()
|
||||||
@@ -70,6 +118,18 @@ const EditScriptNameModal = ({
|
|||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" name="edit_name_modal">
|
<Form form={form} layout="vertical" name="edit_name_modal">
|
||||||
|
<Form.Item
|
||||||
|
name="type"
|
||||||
|
label="类型"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
initialValue={'blank'}
|
||||||
|
>
|
||||||
|
<Radio.Group onChange={typeChange}>
|
||||||
|
<Radio value="blank">空文件</Radio>
|
||||||
|
<Radio value="upload">本地上传</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
{type === 'blank' && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="filename"
|
name="filename"
|
||||||
label="文件名"
|
label="文件名"
|
||||||
@@ -77,17 +137,26 @@ const EditScriptNameModal = ({
|
|||||||
>
|
>
|
||||||
<Input placeholder="请输入文件名" />
|
<Input placeholder="请输入文件名" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
)}
|
||||||
label="父目录"
|
<Form.Item label="父目录" name="path">
|
||||||
name="path"
|
<TreeSelect
|
||||||
initialValue={dirs && dirs.length > 0 ? dirs[0].key : ''}
|
allowClear
|
||||||
>
|
treeData={dirs}
|
||||||
<Select placeholder="请选择父目录">
|
fieldNames={{ value: 'key', label: 'title' }}
|
||||||
{dirs.map((x) => (
|
placeholder="请选择父目录"
|
||||||
<Option value={x.key}>{x.key || '根'}</Option>
|
treeDefaultExpandAll
|
||||||
))}
|
/>
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{type === 'upload' && (
|
||||||
|
<Form.Item label="文件" name="file">
|
||||||
|
<Upload.Dragger beforeUpload={beforeUpload} maxCount={1}>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<UploadOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">点击或者拖拽文件到此区域上传</p>
|
||||||
|
</Upload.Dragger>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
@import '~antd/es/style/themes/default.less';
|
||||||
@import '~@/styles/variable.less';
|
@import '~@/styles/variable.less';
|
||||||
|
|
||||||
.left-tree {
|
.left-tree {
|
||||||
&-container {
|
&-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #fff;
|
background-color: @component-background;
|
||||||
height: calc(100vh - 128px);
|
height: calc(100vh - 128px);
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -13,7 +14,6 @@
|
|||||||
&-scroller {
|
&-scroller {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-right: 1px dashed #f0f0f0;
|
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,3 +22,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.Pane.vertical.Pane1 {
|
||||||
|
padding-right: 5px;
|
||||||
|
border-right: 1px dashed @text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+78
-40
@@ -93,7 +93,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
const getScripts = () => {
|
const getScripts = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}scripts/files`)
|
.get(`${config.apiPrefix}scripts`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setData(data.data);
|
setData(data.data);
|
||||||
setFilterData(data.data);
|
setFilterData(data.data);
|
||||||
@@ -104,7 +104,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
|
|
||||||
const getDetail = (node: any) => {
|
const getDetail = (node: any) => {
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
|
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setValue(data.data);
|
setValue(data.data);
|
||||||
});
|
});
|
||||||
@@ -117,7 +117,6 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
const obj = {
|
const obj = {
|
||||||
node: {
|
node: {
|
||||||
title: s,
|
title: s,
|
||||||
value: s,
|
|
||||||
key: p ? vkey : s,
|
key: p ? vkey : s,
|
||||||
parent: p,
|
parent: p,
|
||||||
},
|
},
|
||||||
@@ -135,7 +134,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
const newMode = value ? LangMap[value.slice(-3)] : '';
|
const newMode = value ? LangMap[value.slice(-3)] : '';
|
||||||
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
|
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
|
||||||
setSelect(node.key);
|
setSelect(node.key);
|
||||||
setTitle(node.parent || node.value);
|
setTitle(node.key);
|
||||||
setCurrentNode(node);
|
setCurrentNode(node);
|
||||||
getDetail(node);
|
getDetail(node);
|
||||||
};
|
};
|
||||||
@@ -209,7 +208,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
<>
|
<>
|
||||||
确认保存文件
|
确认保存文件
|
||||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
{currentNode.value}
|
{currentNode.title}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
,保存后不可恢复
|
,保存后不可恢复
|
||||||
</>
|
</>
|
||||||
@@ -222,7 +221,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}scripts`, {
|
.put(`${config.apiPrefix}scripts`, {
|
||||||
data: {
|
data: {
|
||||||
filename: currentNode.value,
|
filename: currentNode.title,
|
||||||
path: currentNode.parent || '',
|
path: currentNode.parent || '',
|
||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
@@ -262,7 +261,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
request
|
request
|
||||||
.delete(`${config.apiPrefix}scripts`, {
|
.delete(`${config.apiPrefix}scripts`, {
|
||||||
data: {
|
data: {
|
||||||
filename: currentNode.value,
|
filename: currentNode.title,
|
||||||
path: currentNode.parent || '',
|
path: currentNode.parent || '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -278,14 +277,18 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
const index = parentNode.children.findIndex(
|
const index = parentNode.children.findIndex(
|
||||||
(y) => y.key === currentNode.key,
|
(y) => y.key === currentNode.key,
|
||||||
);
|
);
|
||||||
|
if (index !== -1 && parentNodeIndex !== -1) {
|
||||||
parentNode.children.splice(index, 1);
|
parentNode.children.splice(index, 1);
|
||||||
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const index = newData.findIndex(
|
const index = newData.findIndex(
|
||||||
(x) => x.key === currentNode.key,
|
(x) => x.key === currentNode.key,
|
||||||
);
|
);
|
||||||
|
if (index !== -1) {
|
||||||
newData.splice(index, 1);
|
newData.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
setData(newData);
|
setData(newData);
|
||||||
} else {
|
} else {
|
||||||
message.error(_data);
|
message.error(_data);
|
||||||
@@ -311,9 +314,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
) => {
|
) => {
|
||||||
if (filename) {
|
if (filename) {
|
||||||
const newData = [...data];
|
const newData = [...data];
|
||||||
const _file = { title: filename, key, value: filename, parent: path };
|
const _file = { title: filename, key, parent: path };
|
||||||
if (path) {
|
if (path) {
|
||||||
|
// TODO: 更新左侧树数据
|
||||||
const parentNodeIndex = newData.findIndex((x) => x.key === path);
|
const parentNodeIndex = newData.findIndex((x) => x.key === path);
|
||||||
|
if (parentNodeIndex !== -1) {
|
||||||
const parentNode = newData[parentNodeIndex];
|
const parentNode = newData[parentNodeIndex];
|
||||||
if (parentNode.children && parentNode.children.length > 0) {
|
if (parentNode.children && parentNode.children.length > 0) {
|
||||||
parentNode.children.unshift(_file);
|
parentNode.children.unshift(_file);
|
||||||
@@ -321,11 +326,12 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
parentNode.children = [_file];
|
parentNode.children = [_file];
|
||||||
}
|
}
|
||||||
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
newData.unshift(_file);
|
newData.unshift(_file);
|
||||||
}
|
}
|
||||||
setData(newData);
|
setData(newData);
|
||||||
onSelect(_file.value, _file);
|
onSelect(_file.title, _file);
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
}
|
}
|
||||||
setIsAddFileModalVisible(false);
|
setIsAddFileModalVisible(false);
|
||||||
@@ -335,7 +341,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
request
|
request
|
||||||
.post(`${config.apiPrefix}scripts/download`, {
|
.post(`${config.apiPrefix}scripts/download`, {
|
||||||
data: {
|
data: {
|
||||||
filename: currentNode.value,
|
filename: currentNode.title,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((_data: any) => {
|
.then((_data: any) => {
|
||||||
@@ -343,7 +349,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = currentNode.value;
|
a.download = currentNode.title;
|
||||||
document.documentElement.appendChild(a);
|
document.documentElement.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.documentElement.removeChild(a);
|
document.documentElement.removeChild(a);
|
||||||
@@ -367,37 +373,68 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const action = (key: string | number) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'save':
|
||||||
|
saveFile();
|
||||||
|
break;
|
||||||
|
case 'exit':
|
||||||
|
cancelEdit();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuAction = (key: string | number) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'save':
|
||||||
|
addFile();
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
editFile();
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
deleteFile();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const menu = isEditing ? (
|
const menu = isEditing ? (
|
||||||
<Menu>
|
<Menu
|
||||||
<Menu.Item key="save" icon={<PlusOutlined />} onClick={saveFile}>
|
items={[
|
||||||
保存
|
{ label: '保存', key: 'save', icon: <PlusOutlined /> },
|
||||||
</Menu.Item>
|
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
|
||||||
<Menu.Item key="exit" icon={<EditOutlined />} onClick={cancelEdit}>
|
]}
|
||||||
退出编辑
|
onClick={({ key, domEvent }) => {
|
||||||
</Menu.Item>
|
domEvent.stopPropagation();
|
||||||
</Menu>
|
action(key);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Menu>
|
<Menu
|
||||||
<Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}>
|
items={[
|
||||||
新建
|
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
|
||||||
</Menu.Item>
|
{
|
||||||
<Menu.Item
|
label: '编辑',
|
||||||
key="edit"
|
key: 'edit',
|
||||||
icon={<EditOutlined />}
|
icon: <EditOutlined />,
|
||||||
onClick={editFile}
|
disabled: !select,
|
||||||
disabled={!select}
|
},
|
||||||
>
|
{
|
||||||
编辑
|
label: '删除',
|
||||||
</Menu.Item>
|
key: 'delete',
|
||||||
<Menu.Item
|
icon: <DeleteOutlined />,
|
||||||
key="delete"
|
disabled: !select,
|
||||||
icon={<DeleteOutlined />}
|
},
|
||||||
onClick={deleteFile}
|
]}
|
||||||
disabled={!select}
|
onClick={({ key, domEvent }) => {
|
||||||
>
|
domEvent.stopPropagation();
|
||||||
删除
|
menuAction(key);
|
||||||
</Menu.Item>
|
}}
|
||||||
</Menu>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -414,6 +451,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||||
treeData={data}
|
treeData={data}
|
||||||
placeholder="请选择脚本"
|
placeholder="请选择脚本"
|
||||||
|
fieldNames={{ value: 'key', label: 'title' }}
|
||||||
showSearch
|
showSearch
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user