mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 10:40:52 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
87231fac25 | ||
|
|
2a5f0eee92 | ||
|
|
df2a659050 | ||
|
|
adc8f35e83 | ||
|
|
dfb0d73ef3 | ||
|
|
3a8101aad1 | ||
|
|
65d6432dad | ||
|
|
4929fa0440 | ||
|
|
7a02fe0d8e | ||
|
|
d06fccd0d1 | ||
|
|
8be71b129a | ||
|
|
1b02668014 | ||
|
|
3f536b5062 | ||
|
|
c3517ccc41 | ||
|
|
bdc3763a6f | ||
|
|
fcfdee4988 | ||
|
|
eb15e096e3 | ||
|
|
6e36e06dda | ||
|
|
cbd1ddc5e7 | ||
|
|
ebc2e0e4b6 | ||
|
|
95e104dc9c | ||
|
|
99f02e7c54 | ||
|
|
bc110ff949 | ||
|
|
69a5ba8996 | ||
|
|
25236958ff | ||
|
|
f2dbb64984 | ||
|
|
069e236674 | ||
|
|
e06175c3da | ||
|
|
dfab2cb7f1 | ||
|
|
f1e0823a10 | ||
|
|
5fbad9956f | ||
|
|
d6f5198893 | ||
|
|
603dc07581 | ||
|
|
ace820efd9 | ||
|
|
6ed4aa6bf4 | ||
|
|
9e4b41ce48 | ||
|
|
d8587b64a5 | ||
|
|
eba78ca84f | ||
|
|
5fd79173d6 | ||
|
|
b86ee5674d | ||
|
|
42fbed157f | ||
|
|
c51306ef27 | ||
|
|
e260dcb985 | ||
|
|
2ddf69a79a | ||
|
|
2f4fb657e6 | ||
|
|
2a9751e051 | ||
|
|
43e02905c8 | ||
|
|
635306e2d3 | ||
|
|
63f21b1698 | ||
|
|
59c9b17c40 | ||
|
|
dc5b4de5fa | ||
|
|
5d846beabd | ||
|
|
9728119101 | ||
|
|
38609feee9 | ||
|
|
0b99882193 | ||
|
|
e03a9d2c1d | ||
|
|
105c0f802f | ||
|
|
da70b0799a | ||
|
|
9d89ecc51e | ||
|
|
c77e302874 | ||
|
|
fd7153403d | ||
|
|
eff109bf81 | ||
|
|
1614f3c8ad | ||
|
|
7ced425e01 | ||
|
|
b183e9cd29 | ||
|
|
9320074a02 | ||
|
|
ecdfdff50d | ||
|
|
20ddb5a3cf | ||
|
|
9d8c592e69 | ||
|
|
66266fd545 | ||
|
|
cc238dedbc | ||
|
|
dac694776d | ||
|
|
1f42d0245c | ||
|
|
a6319698e2 | ||
|
|
9ab702e344 | ||
|
|
49045c39c7 | ||
|
|
52490bf320 | ||
|
|
faf982b156 | ||
|
|
cad1d86caf | ||
|
|
dd424237e2 | ||
|
|
0214c6f81b | ||
|
|
f7e1c2ae5e | ||
|
|
5e52398a43 | ||
|
|
7bc24a4b62 | ||
|
|
105073ec23 | ||
|
|
bbb1fc4e1b | ||
|
|
381427fd60 | ||
|
|
6c63fb83ef | ||
|
|
602ffdcd5a | ||
|
|
72240a7834 | ||
|
|
8336434d3f | ||
|
|
dd2d3127a0 | ||
|
|
c960717d92 | ||
|
|
23db56df57 | ||
|
|
ed95d66441 | ||
|
|
6cd0feaace | ||
|
|
22a23fe05e | ||
|
|
517fdcc8ec | ||
|
|
71d7d3818f | ||
|
|
7b7d62760e | ||
|
|
f554a5b1a0 | ||
|
|
f26d6f1449 | ||
|
|
0728f1f231 | ||
|
|
e687511f32 | ||
|
|
a8a2a54bea | ||
|
|
c1a3ac6e4d | ||
|
|
b1d55422ac | ||
|
|
f6630d84ab | ||
|
|
5c2bbc941f | ||
|
|
53123af832 | ||
|
|
41c3f21863 | ||
|
|
356f8db728 | ||
|
|
a14a8cc067 | ||
|
|
7ecd38ad1f | ||
|
|
4bc1deda42 | ||
|
|
f9c1dfc8cb | ||
|
|
7d417a11c9 | ||
|
|
a35c0cb64e | ||
|
|
1aaa0f0fa6 | ||
|
|
2fe8c38293 | ||
|
|
2a1adb2e0e | ||
|
|
5b76213354 | ||
|
|
ebcf8f96c8 | ||
|
|
0c3b3c206e | ||
|
|
ef16622a7e | ||
|
|
e7aff6d1d6 | ||
|
|
b1bee910b2 | ||
|
|
2049cf9aa9 | ||
|
|
6c3f848b2c | ||
|
|
2f8d31658b | ||
|
|
c77eb3d28d | ||
|
|
1a801c2d31 | ||
|
|
0a6c7e3719 | ||
|
|
9f5c8fa70a | ||
|
|
ecb035d0b4 | ||
|
|
b78b382ba1 | ||
|
|
49088f0d3a | ||
|
|
0abf657a9c | ||
|
|
eb2390135f | ||
|
|
0753ef9ce9 | ||
|
|
f3aca5575c | ||
|
|
54dd82dc77 | ||
|
|
86e5f208fb | ||
|
|
ce3543779e | ||
|
|
e11d0c25f3 | ||
|
|
e03c64dceb | ||
|
|
4d4d31431d | ||
|
|
c9fecae67a | ||
|
|
173b87a7b1 | ||
|
|
49ab1ad93a | ||
|
|
6c943ac97a | ||
|
|
325b423c4d | ||
|
|
164126ddc7 | ||
|
|
e728faf519 | ||
|
|
b00acc7ef5 | ||
|
|
e44a7aca1a | ||
|
|
aef57d94d6 | ||
|
|
9124f1acf3 | ||
|
|
01e0a89ba9 | ||
|
|
3052b48f51 | ||
|
|
d630f34505 | ||
|
|
2692351e0a | ||
|
|
0fade7a5a9 |
+7
-4
@@ -1,9 +1,12 @@
|
|||||||
MONGODB_URI='mongodb://'
|
PUBLIC_PORT=5400
|
||||||
YIYAN_MONGODB_URI=''
|
|
||||||
|
|
||||||
CRON_PORT=5500
|
CRON_PORT=5500
|
||||||
PORT=5600
|
BACK_PORT=5600
|
||||||
|
PORT=5700
|
||||||
|
|
||||||
LOG_LEVEL='debug'
|
LOG_LEVEL='debug'
|
||||||
|
|
||||||
SECRET='whyour'
|
SECRET='whyour'
|
||||||
|
|
||||||
|
QINIU_AK=''
|
||||||
|
QINIU_SK=''
|
||||||
|
QINIU_SCOPE=''
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
name: build static
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
|
|
||||||
- name: build front and back
|
|
||||||
run: |
|
|
||||||
yarn install
|
|
||||||
yarn build
|
|
||||||
yarn build-back
|
|
||||||
|
|
||||||
- name: copy to static repo
|
|
||||||
env:
|
|
||||||
# GITHUB_REPO: gitee.com/whyour/qinglong-static
|
|
||||||
GITHUB_REPO: github.com/whyour/qinglong-static
|
|
||||||
run: |
|
|
||||||
mkdir -p static
|
|
||||||
cd ./static
|
|
||||||
cp -rf ../dist ./ && cp -rf ../build ./
|
|
||||||
git init && git add .
|
|
||||||
git config user.name "whyour"
|
|
||||||
git config user.email "imwhyour@gmail.com"
|
|
||||||
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
|
||||||
git push --force --quiet "https://whyour:${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" master:master
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
name: Build And Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
- 'develop'
|
||||||
|
# Sequence of patterns matched against refs/tags
|
||||||
|
tags:
|
||||||
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
schedule:
|
||||||
|
# 参考 https://jasonet.co/posts/scheduled-actions/
|
||||||
|
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
|
||||||
|
# minute hour day month dayOfWeek
|
||||||
|
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
|
||||||
|
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-static:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
|
||||||
|
- name: build front and back
|
||||||
|
run: |
|
||||||
|
npm i -g pnpm
|
||||||
|
pnpm install
|
||||||
|
pnpm build:front
|
||||||
|
pnpm build:back
|
||||||
|
|
||||||
|
- name: copy to static repo
|
||||||
|
env:
|
||||||
|
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
||||||
|
GITHUB_BRANCH: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
mkdir -p tmp
|
||||||
|
cd ./tmp
|
||||||
|
cp -rf ../static/* ./
|
||||||
|
git init -b ${GITHUB_BRANCH} && git add .
|
||||||
|
git config --local user.name 'github-actions[bot]'
|
||||||
|
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
||||||
|
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: build-static
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
|
||||||
|
- 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@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ github.repository }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
# generate Docker tags based on the following events/attributes
|
||||||
|
# nightly, master, pr-2, 1.2.3, 1.2, 1
|
||||||
|
tags: |
|
||||||
|
type=schedule,pattern=nightly
|
||||||
|
type=edge
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
build-args: |
|
||||||
|
MAINTAINER=${{ github.repository_owner }}
|
||||||
|
QL_BRANCH=${{ github.ref_name }}
|
||||||
|
network: host
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
|
context: docker/
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
name: Publish Docker Image
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout base
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
# https://github.com/docker/setup-buildx-action
|
|
||||||
- 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 }}
|
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
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/
|
|
||||||
|
|
||||||
- name: Replace tag without `v`
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
uses: actions/github-script@v1
|
|
||||||
id: version
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
return context.payload.ref.replace(/\/?refs\/tags\/v/, '')
|
|
||||||
result-encoding: string
|
|
||||||
|
|
||||||
- name: Docker buildx image and push on release
|
|
||||||
env:
|
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
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:${{steps.version.outputs.result}} docker/
|
|
||||||
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/
|
|
||||||
+7
-3
@@ -5,11 +5,12 @@
|
|||||||
/npm-debug.log*
|
/npm-debug.log*
|
||||||
/yarn-error.log
|
/yarn-error.log
|
||||||
/yarn.lock
|
/yarn.lock
|
||||||
|
/pnpm-lock.yaml
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/dist
|
/static
|
||||||
/build
|
/data
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -21,8 +22,11 @@
|
|||||||
/.env.local
|
/.env.local
|
||||||
.env
|
.env
|
||||||
.history
|
.history
|
||||||
|
.version.ts
|
||||||
|
|
||||||
/config
|
/config
|
||||||
/log
|
/log
|
||||||
/db
|
/db
|
||||||
/manual_log
|
/manual_log
|
||||||
|
/scripts
|
||||||
|
/bak
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
|
||||||
|
strict-peer-dependencies=false
|
||||||
@@ -4,16 +4,27 @@ 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: {},
|
||||||
favicon: 'https://qinglong.whyour.cn/g5.ico',
|
esbuild: {},
|
||||||
|
webpack5: {},
|
||||||
|
dynamicImport: {
|
||||||
|
loading: '@/components/pageLoading',
|
||||||
|
},
|
||||||
|
favicon: '/images/g5.ico',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api/public': {
|
||||||
target: 'http://127.0.0.1:5678/',
|
target: 'http://127.0.0.1:5400/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:5600/',
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
chainWebpack: (config) => {
|
chainWebpack: (config) => {
|
||||||
config.plugin('compression-webpack-plugin').use(
|
config.plugin('compression-webpack-plugin').use(
|
||||||
@@ -28,14 +39,9 @@ export default defineConfig({
|
|||||||
externals: {
|
externals: {
|
||||||
react: 'window.React',
|
react: 'window.React',
|
||||||
'react-dom': 'window.ReactDOM',
|
'react-dom': 'window.ReactDOM',
|
||||||
codemirror: 'window.CodeMirror',
|
|
||||||
darkreader: 'window.DarkReader',
|
|
||||||
},
|
},
|
||||||
scripts: [
|
scripts: [
|
||||||
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
||||||
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
|
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/lib/codemirror.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/darkreader@4.9.27/darkreader.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/mode/shell/shell.js',
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,64 +1,210 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/whyour/qinglong">
|
<a href="https://github.com/whyour/qinglong">
|
||||||
<img width="150" src="https://qinglong.whyour.cn/qinglong.png">
|
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">青龙(WIP)</h1>
|
<h1 align="center">青龙</h1>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
python和javaScript的定时任务管理面板
|
支持python3、javaScript、shell、typescript 的定时任务管理面板
|
||||||
|
|
||||||
[![donate][donate-image]][donate-url] [![build status][build-status-image]][build-status-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker version][docker-version-image]][docker-version-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]
|
||||||
|
|
||||||
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=for-the-badge
|
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||||
[donate-url]: https://qinglong.whyour.cn/nice.png
|
|
||||||
[build-status-image]: https://img.shields.io/docker/cloud/build/whyour/qinglong?style=for-the-badge
|
|
||||||
[build-status-url]: https://img.shields.io/docker/cloud/build/whyour/qinglong
|
|
||||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=for-the-badge
|
|
||||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=for-the-badge
|
[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-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=for-the-badge
|
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
||||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=for-the-badge
|
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
[](https://whyour.cn)
|
||||||
|
|
||||||
|
简体中文 | [English](./README-en.md)
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 支持多种脚本语言(python3、javaScript、shell、typescript)
|
||||||
|
- 支持在线管理脚本、环境变量、配置文件
|
||||||
|
- 支持在线查看任务日志
|
||||||
|
- 支持秒级任务设置
|
||||||
|
- 支持系统级通知
|
||||||
|
- 支持暗黑模式
|
||||||
|
- 支持手机端操作
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
### 本机部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 待完善
|
||||||
|
```
|
||||||
|
|
||||||
|
### podman 部署
|
||||||
|
|
||||||
|
1. podman 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
https://podman.io/getting-started/installation
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
podman run -dit \
|
||||||
|
--network bridge \
|
||||||
|
-v $PWD/ql:/ql/data \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
--restart unless-stopped \
|
||||||
|
docker.io/whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### docker 部署
|
||||||
|
|
||||||
|
1. docker 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -sSL get.docker.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 配置国内镜像源
|
||||||
|
Configure domestic mirror sources
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /etc/docker
|
||||||
|
tee /etc/docker/daemon.json <<-'EOF'
|
||||||
|
{
|
||||||
|
"registry-mirrors": [
|
||||||
|
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
||||||
|
"https://ypzju6vq.mirror.aliyuncs.com",
|
||||||
|
"https://registry.docker-cn.com",
|
||||||
|
"http://hub-mirror.c.163.com",
|
||||||
|
"https://docker.mirrors.ustc.edu.cn"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 启动容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -dit \
|
||||||
|
-v $PWD/ql:/ql/data \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
--restart unless-stopped \
|
||||||
|
whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### docker-compose 部署
|
||||||
|
|
||||||
|
1. docker-compose 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir qinglong
|
||||||
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
docker-compose up -d
|
||||||
|
# 停止
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
1. 内置命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新并重启青龙
|
||||||
|
ql update
|
||||||
|
# 运行自定义脚本extra.sh
|
||||||
|
ql extra
|
||||||
|
# 添加单个脚本文件
|
||||||
|
ql raw <file_url>
|
||||||
|
# 添加单个仓库的指定脚本
|
||||||
|
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
||||||
|
# 删除旧日志
|
||||||
|
ql rmlog <days>
|
||||||
|
# 启动tg-bot
|
||||||
|
ql bot
|
||||||
|
# 检测青龙环境并修复
|
||||||
|
ql check
|
||||||
|
# 重置登录错误次数
|
||||||
|
ql resetlet
|
||||||
|
# 禁用两步登录
|
||||||
|
ql resettfa
|
||||||
|
|
||||||
|
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||||
|
task <file_path>
|
||||||
|
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||||
|
task <file_path> now
|
||||||
|
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
||||||
|
task <file_path> conc <env_name> <account_number>(可选的)
|
||||||
|
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
||||||
|
task <file_path> desi <env_name> <account_number>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 参数说明
|
||||||
|
|
||||||
|
* file_url: 脚本地址
|
||||||
|
* repo_url: 仓库地址
|
||||||
|
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
|
||||||
|
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
|
||||||
|
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
|
||||||
|
* extensions: 拉取仓库的文件后缀,多个竖线分割
|
||||||
|
* branch: 拉取仓库的分支
|
||||||
|
* days: 需要保留的日志的天数
|
||||||
|
* file_path: 任务执行时的文件路径
|
||||||
|
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||||
|
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||||
|
|
||||||
|
## 链接
|
||||||
|
|
||||||
|
- [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)
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone git@github.com:whyour/qinglong.git
|
||||||
|
$ cd qinglong
|
||||||
|
$ cp .env.example .env
|
||||||
|
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
||||||
|
$ npm install -g pnpm
|
||||||
|
$ pnpm install
|
||||||
|
$ pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
打开你的浏览器,访问 http://127.0.0.1:5700
|
||||||
|
|
||||||
|
## 交流
|
||||||
|
|
||||||
|
[telegram频道](https://t.me/jiao_long)
|
||||||
|
|
||||||
|
## 名称来源
|
||||||
|
|
||||||
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
||||||
|
|
||||||
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
|
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
|
||||||
|
|
||||||
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
|
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
|
||||||
|
|
||||||
## 多谢
|
|
||||||
|
|
||||||
* [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)
|
|
||||||
|
|
||||||
## 免责声明
|
|
||||||
|
|
||||||
1. 此仓储脚本仅用于学习研究,不保证其合法性、准确性、有效性,请根据情况自行判断,本人对此不承担任何保证责任。
|
|
||||||
|
|
||||||
2. 由于此仓储脚本仅用于学习研究,您必须在下载后 24 小时内将所有内容从您的计算机或手机或任何存储设备中完全删除,若违反规定引起任何事件本人对此均不负责。
|
|
||||||
|
|
||||||
3. 请勿将此仓储脚本用于任何商业或非法目的,若违反规定请自行对此负责。
|
|
||||||
|
|
||||||
4. 此仓储脚本涉及应用与本人无关,本人对因此引起的任何隐私泄漏或其他后果不承担任何责任。
|
|
||||||
|
|
||||||
5. 本人对任何脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失和损害。
|
|
||||||
|
|
||||||
6. 如果任何单位或个人认为此仓储脚本可能涉嫌侵犯其权利,应及时通知并提供身份证明,所有权证明,我们将在收到认证文件确认后删除此仓储脚本。
|
|
||||||
|
|
||||||
7. 所有直接或间接使用、查看此仓储脚本的人均应该仔细阅读此声明。本人保留随时更改或补充此声明的权利。一旦您使用或复制了此仓储脚本,即视为您已接受此免责声明。
|
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { Logger } from 'winston';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import config from '../config';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import { createPassword } from '../config/util';
|
|
||||||
const route = Router();
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
|
||||||
app.use('/', route);
|
|
||||||
route.post(
|
|
||||||
'/login',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
let username = req.body.username;
|
|
||||||
let password = req.body.password;
|
|
||||||
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
|
|
||||||
if (err) console.log(err);
|
|
||||||
const authInfo = JSON.parse(data);
|
|
||||||
if (username && password) {
|
|
||||||
if (
|
|
||||||
authInfo.username === 'admin' &&
|
|
||||||
authInfo.password === 'adminadmin'
|
|
||||||
) {
|
|
||||||
const newPassword = createPassword(16, 22);
|
|
||||||
fs.writeFileSync(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: authInfo.username,
|
|
||||||
password: newPassword,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return res.send({
|
|
||||||
code: 100,
|
|
||||||
msg: '已初始化密码,请前往auth.json查看并重新登录',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
username == authInfo.username &&
|
|
||||||
password == authInfo.password
|
|
||||||
) {
|
|
||||||
let token = jwt.sign(
|
|
||||||
{ username, password },
|
|
||||||
config.secret as any,
|
|
||||||
{ expiresIn: 60 * 60 * 24 * 7, algorithm: 'HS384' },
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: authInfo.username,
|
|
||||||
password: authInfo.password,
|
|
||||||
token,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
res.send({ code: 200, token });
|
|
||||||
} else {
|
|
||||||
res.send({ code: 400, msg: config.authError });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.send({ err: 400, msg: '请输入用户名密码!' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/logout',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
|
|
||||||
if (err) console.log(err);
|
|
||||||
const authInfo = JSON.parse(data);
|
|
||||||
fs.writeFileSync(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: authInfo.username,
|
|
||||||
password: authInfo.password,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
res.send({ code: 200 });
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/user',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
fs.writeFile(config.authConfigFile, JSON.stringify(req.body), (err) => {
|
|
||||||
if (err) console.log(err);
|
|
||||||
res.send({ code: 200, msg: '更新成功' });
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/user',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
|
||||||
if (err) console.log(err);
|
|
||||||
const authInfo = JSON.parse(data);
|
|
||||||
res.send({ code: 200, data: { username: authInfo.username } });
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+41
-27
@@ -5,36 +5,51 @@ 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 { execSync } from 'child_process';
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/', route);
|
app.use('/configs', route);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/config/:key',
|
'/files',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let content = '未找到文件';
|
const fileList = fs.readdirSync(config.configPath, 'utf-8');
|
||||||
switch (req.params.key) {
|
res.send({
|
||||||
case 'config':
|
code: 200,
|
||||||
content = getFileContentByName(config.confFile);
|
data: fileList
|
||||||
break;
|
.filter((x) => !config.blackFileList.includes(x))
|
||||||
case 'sample':
|
.map((x) => {
|
||||||
content = getFileContentByName(config.sampleFile);
|
return { title: x, value: x };
|
||||||
break;
|
}),
|
||||||
case 'crontab':
|
});
|
||||||
content = getFileContentByName(config.crontabFile);
|
} catch (e) {
|
||||||
break;
|
return next(e);
|
||||||
case 'extra':
|
}
|
||||||
content = getFileContentByName(config.extraFile);
|
},
|
||||||
break;
|
);
|
||||||
default:
|
|
||||||
break;
|
route.get(
|
||||||
|
'/:file',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let content = '';
|
||||||
|
if (config.blackFileList.includes(req.params.file)) {
|
||||||
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
|
}
|
||||||
|
if (req.params.file.includes('sample')) {
|
||||||
|
content = getFileContentByName(
|
||||||
|
`${config.samplePath}${req.params.file}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = getFileContentByName(
|
||||||
|
`${config.configPath}${req.params.file}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -45,21 +60,20 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
content: Joi.string().required(),
|
content: Joi.string().allow('').optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
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 { name, content } = req.body;
|
const { name, content } = req.body;
|
||||||
const path = (config.fileMap as any)[name];
|
if (config.blackFileList.includes(name)) {
|
||||||
fs.writeFileSync(path, content);
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
if (name === 'crontab.list') {
|
|
||||||
execSync(`crontab ${path}`);
|
|
||||||
}
|
}
|
||||||
res.send({ code: 200, msg: '保存成功' });
|
const path = `${config.configPath}${name}`;
|
||||||
|
fs.writeFileSync(path, content);
|
||||||
|
res.send({ code: 200, message: '保存成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+142
-54
@@ -7,31 +7,28 @@ import cron_parser from 'cron-parser';
|
|||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/', route);
|
app.use('/crons', route);
|
||||||
route.get(
|
|
||||||
'/crons',
|
route.get('/', 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 cronService = Container.get(CronService);
|
||||||
const cronService = Container.get(CronService);
|
const data = await cronService.crontabs(req.query.searchValue as string);
|
||||||
const data = await cronService.crontabs(
|
return res.send({ code: 200, data });
|
||||||
req.query.searchValue as string,
|
} catch (e) {
|
||||||
);
|
logger.error('🔥 error: %o', e);
|
||||||
return res.send({ code: 200, data });
|
return next(e);
|
||||||
} catch (e) {
|
}
|
||||||
logger.error('🔥 error: %o', e);
|
});
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/crons',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
command: Joi.string().required(),
|
command: Joi.string().required(),
|
||||||
schedule: Joi.string().required(),
|
schedule: Joi.string().required(),
|
||||||
name: Joi.string().optional(),
|
name: Joi.string().optional(),
|
||||||
|
labels: Joi.array().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -45,16 +42,15 @@ 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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/crons/run',
|
'/run',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.string().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -63,16 +59,15 @@ 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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/crons/stop',
|
'/stop',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.string().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -81,16 +76,58 @@ 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/labels',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
ids: Joi.array().items(Joi.number().required()),
|
||||||
|
labels: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.removeLabels(
|
||||||
|
req.body.ids,
|
||||||
|
req.body.labels,
|
||||||
|
);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/labels',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
ids: Joi.array().items(Joi.number().required()),
|
||||||
|
labels: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.addLabels(req.body.ids, req.body.labels);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/crons/disable',
|
'/disable',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.string().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -99,16 +136,15 @@ 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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/crons/enable',
|
'/enable',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.string().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -117,40 +153,39 @@ 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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/crons/:id/log',
|
'/:id/log',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.string().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/crons',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
command: Joi.string().optional(),
|
labels: Joi.array().optional().allow(null),
|
||||||
schedule: Joi.string().optional(),
|
command: Joi.string().required(),
|
||||||
name: Joi.string().optional(),
|
schedule: Joi.string().required(),
|
||||||
_id: Joi.string().required(),
|
name: Joi.string().optional().allow(null),
|
||||||
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -167,16 +202,15 @@ 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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/crons',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.string().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -185,14 +219,47 @@ 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/pin',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.pin(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/unpin',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.unPin(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/crons/import',
|
'/import',
|
||||||
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 {
|
||||||
@@ -200,38 +267,40 @@ 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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/crons/:id',
|
'/:id',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.string().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.get(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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/crons/status',
|
'/status',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
ids: Joi.array().items(Joi.string().required()),
|
ids: Joi.array().items(Joi.number().required()),
|
||||||
status: Joi.string().required(),
|
status: Joi.string().required(),
|
||||||
|
pid: Joi.string().optional().allow(null),
|
||||||
|
log_path: Joi.string().optional().allow(null),
|
||||||
|
last_running_time: Joi.number().optional().allow(null),
|
||||||
|
last_execution_time: Joi.number().optional().allow(null),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -241,10 +310,29 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.status({
|
const data = await cronService.status({
|
||||||
...req.body,
|
...req.body,
|
||||||
status: parseInt(req.body.status),
|
status: parseInt(req.body.status),
|
||||||
|
pid: parseInt(req.body.pid) || '',
|
||||||
});
|
});
|
||||||
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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import DependenceService from '../services/dependence';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/dependencies', route);
|
||||||
|
|
||||||
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.dependencies(req.query as any);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
type: Joi.number().required(),
|
||||||
|
remark: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.create(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
id: Joi.number().required(),
|
||||||
|
type: Joi.number().required(),
|
||||||
|
remark: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.update(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} 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 dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.remove(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/force',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.remove(req.body, true);
|
||||||
|
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 dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.getDb({ id: req.params.id });
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/reinstall',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.reInstall(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,179 +1,178 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import CookieService from '../services/cookie';
|
import EnvService from '../services/env';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/', route);
|
app.use('/envs', route);
|
||||||
route.get(
|
|
||||||
'/cookies',
|
route.get('/', 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 envService = Container.get(EnvService);
|
||||||
const cookieService = Container.get(CookieService);
|
const data = await envService.envs(req.query.searchValue as string);
|
||||||
const data = await cookieService.cookies('', { position: -1 }, true);
|
return res.send({ code: 200, data });
|
||||||
return res.send({ code: 200, data });
|
} catch (e) {
|
||||||
} catch (e) {
|
logger.error('🔥 error: %o', e);
|
||||||
logger.error('🔥 error: %o', e);
|
return next(e);
|
||||||
return next(e);
|
}
|
||||||
}
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/cookies',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.string().required()).min(1),
|
body: Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
value: Joi.string().required(),
|
||||||
|
name: Joi.string().required(),
|
||||||
|
remarks: 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 {
|
||||||
const cookieService = Container.get(CookieService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await cookieService.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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/cookies',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
value: Joi.string().required(),
|
value: Joi.string().required(),
|
||||||
_id: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
|
remarks: Joi.string().optional().allow('').allow(null),
|
||||||
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
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 cookieService = Container.get(CookieService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await cookieService.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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/cookies',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.string().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
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 cookieService = Container.get(CookieService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await cookieService.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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/cookies/:id/move',
|
'/:id/move',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.string().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
fromIndex: Joi.number().required(),
|
fromIndex: Joi.number().required(),
|
||||||
toIndex: Joi.number().required(),
|
toIndex: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cookieService = Container.get(CookieService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await cookieService.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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/cookies/:id/refresh',
|
|
||||||
celebrate({
|
|
||||||
params: Joi.object({
|
|
||||||
id: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const cookieService = Container.get(CookieService);
|
|
||||||
const data = await cookieService.refreshCookie(req.params.id);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/cookies/disable',
|
'/disable',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.string().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
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 cookieService = Container.get(CookieService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await cookieService.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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/cookies/enable',
|
'/enable',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.string().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
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 cookieService = Container.get(CookieService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await cookieService.enabled(req.body);
|
const data = await envService.enabled(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/name',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
ids: Joi.array().items(Joi.number().required()),
|
||||||
|
name: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/cookies/:id',
|
'/:id',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.string().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cookieService = Container.get(CookieService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await cookieService.get(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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
+14
-4
@@ -1,17 +1,27 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import auth from './auth';
|
import user from './user';
|
||||||
import cookie from './cookie';
|
import env from './env';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import log from './log';
|
import log from './log';
|
||||||
import cron from './cron';
|
import cron from './cron';
|
||||||
|
import script from './script';
|
||||||
|
import open from './open';
|
||||||
|
import dependence from './dependence';
|
||||||
|
import system from './system';
|
||||||
|
import subscription from './subscription';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
auth(app);
|
user(app);
|
||||||
cookie(app);
|
env(app);
|
||||||
config(app);
|
config(app);
|
||||||
log(app);
|
log(app);
|
||||||
cron(app);
|
cron(app);
|
||||||
|
script(app);
|
||||||
|
open(app);
|
||||||
|
dependence(app);
|
||||||
|
system(app);
|
||||||
|
subscription(app);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-56
@@ -3,73 +3,44 @@ 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('/', route);
|
app.use('/logs', route);
|
||||||
|
|
||||||
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const result = readDirs(config.logPath, config.logPath, blacklist);
|
||||||
|
res.send({
|
||||||
|
code: 200,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/logs',
|
'/: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 fileList = fs.readdirSync(config.logPath, 'utf-8');
|
if (blacklist.includes(req.path)) {
|
||||||
const dirs = [];
|
return res.send({ code: 403, message: '暂无权限' });
|
||||||
for (let i = 0; i < fileList.length; i++) {
|
|
||||||
const stat = fs.lstatSync(config.logPath + fileList[i]);
|
|
||||||
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 });
|
const filePath = join(
|
||||||
} catch (e) {
|
config.logPath,
|
||||||
logger.error('🔥 error: %o', e);
|
(req.query.path || '') as string,
|
||||||
return next(e);
|
req.params.file,
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/logs/: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}`,
|
|
||||||
);
|
);
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/logs/:file',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const { file } = req.params;
|
|
||||||
const content = getFileContentByName(`${config.logPath}/${file}`);
|
|
||||||
res.send({ code: 200, data: content });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import OpenService from '../services/open';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/', route);
|
||||||
|
route.get(
|
||||||
|
'/apps',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const data = await openService.list();
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/apps',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
name: Joi.string().optional().allow('').disallow('system'),
|
||||||
|
scopes: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const data = await openService.create(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/apps',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
name: Joi.string().optional().allow(''),
|
||||||
|
scopes: Joi.array().items(Joi.string()),
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const data = await openService.update(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/apps',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const data = await openService.remove(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/apps/:id/reset-secret',
|
||||||
|
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 openService = Container.get(OpenService);
|
||||||
|
const data = await openService.resetSecret(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/auth/token',
|
||||||
|
celebrate({
|
||||||
|
query: {
|
||||||
|
client_id: Joi.string().required(),
|
||||||
|
client_secret: Joi.string().required(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const result = await openService.authToken(req.query as any);
|
||||||
|
return res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import {
|
||||||
|
fileExist,
|
||||||
|
getFileContentByName,
|
||||||
|
readDirs,
|
||||||
|
getLastModifyFilePath,
|
||||||
|
readDir,
|
||||||
|
} from '../config/util';
|
||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import config from '../config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import path, { join, parse } from 'path';
|
||||||
|
import ScriptService from '../services/script';
|
||||||
|
import multer from 'multer';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.scriptPath);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, file.originalname);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/scripts', route);
|
||||||
|
|
||||||
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let result = [];
|
||||||
|
const blacklist = ['node_modules', '.git'];
|
||||||
|
if (req.query.path) {
|
||||||
|
const targetPath = path.join(
|
||||||
|
config.scriptPath,
|
||||||
|
req.query.path as string,
|
||||||
|
);
|
||||||
|
result = readDir(targetPath, config.scriptPath, blacklist);
|
||||||
|
} else {
|
||||||
|
result = readDirs(config.scriptPath, config.scriptPath, blacklist);
|
||||||
|
}
|
||||||
|
res.send({
|
||||||
|
code: 200,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/:file',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const filePath = join(
|
||||||
|
config.scriptPath,
|
||||||
|
req.query.path as string,
|
||||||
|
req.params.file,
|
||||||
|
);
|
||||||
|
const content = getFileContentByName(filePath);
|
||||||
|
res.send({ code: 200, data: content });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/',
|
||||||
|
upload.single('file'),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, path, content, originFilename } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
originFilename: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
path = config.scriptPath;
|
||||||
|
}
|
||||||
|
if (!path.endsWith('/')) {
|
||||||
|
path += '/';
|
||||||
|
}
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
path = `${config.scriptPath}${path}`;
|
||||||
|
}
|
||||||
|
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||||
|
return res.send({
|
||||||
|
code: 430,
|
||||||
|
message: '文件路径禁止访问',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
fs.renameSync(req.file.path, join(path, req.file.filename));
|
||||||
|
return res.send({ code: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!originFilename) {
|
||||||
|
originFilename = filename;
|
||||||
|
}
|
||||||
|
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
|
||||||
|
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
||||||
|
if (fs.existsSync(originFilePath)) {
|
||||||
|
fs.copyFileSync(
|
||||||
|
originFilePath,
|
||||||
|
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
|
||||||
|
);
|
||||||
|
if (filename !== originFilename) {
|
||||||
|
fs.unlinkSync(originFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
return res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
content: Joi.string().required().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, content, path } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
content: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
return res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, path } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/download',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
};
|
||||||
|
const filePath = `${config.scriptPath}${filename}`;
|
||||||
|
// const stats = fs.statSync(filePath);
|
||||||
|
// res.set({
|
||||||
|
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
||||||
|
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件
|
||||||
|
// 'Content-Length': stats.size //文件大小
|
||||||
|
// });
|
||||||
|
// fs.createReadStream(filePath).pipe(res);
|
||||||
|
return res.download(filePath, filename, (err) => {
|
||||||
|
return next(err);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/run',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
content: Joi.string().optional().allow(''),
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, content, path } = req.body;
|
||||||
|
const { name, ext } = parse(filename);
|
||||||
|
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||||
|
fs.writeFileSync(filePath, content || '', { encoding: 'utf8' });
|
||||||
|
|
||||||
|
const scriptService = Container.get(ScriptService);
|
||||||
|
const result = await scriptService.runScript(filePath);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/stop',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
content: Joi.string().optional().allow(''),
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, content, path } = req.body;
|
||||||
|
const { name, ext } = parse(filename);
|
||||||
|
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||||
|
|
||||||
|
const scriptService = Container.get(ScriptService);
|
||||||
|
const result = await scriptService.stopScript(filePath);
|
||||||
|
res.send(result);
|
||||||
|
} catch (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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import config from '../config';
|
||||||
|
import SystemService from '../services/system';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import UserService from '../services/user';
|
||||||
|
import { EnvModel } from '../data/env';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/system', route);
|
||||||
|
|
||||||
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const authInfo = await userService.getUserInfo();
|
||||||
|
const envCount = await EnvModel.count();
|
||||||
|
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||||
|
|
||||||
|
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||||
|
const version = currentVersionFile.match(versionRegx)![1];
|
||||||
|
|
||||||
|
let isInitialized = true;
|
||||||
|
if (
|
||||||
|
Object.keys(authInfo).length === 2 &&
|
||||||
|
authInfo.username === 'admin' &&
|
||||||
|
authInfo.password === 'admin' &&
|
||||||
|
envCount === 0
|
||||||
|
) {
|
||||||
|
isInitialized = false;
|
||||||
|
}
|
||||||
|
res.send({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
isInitialized,
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/log/remove',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const data = await systemService.getLogRemoveFrequency();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/log/remove',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
frequency: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.updateLogRemoveFrequency(
|
||||||
|
req.body.frequency,
|
||||||
|
);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/update-check',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.checkUpdate();
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/update',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.updateSystem();
|
||||||
|
res.send(result);
|
||||||
|
} catch (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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import UserService from '../services/user';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import multer from 'multer';
|
||||||
|
import path from 'path';
|
||||||
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
import config from '../config';
|
||||||
|
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) => {
|
||||||
|
app.use('/user', route);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/login',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
username: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.login({ ...req.body }, req);
|
||||||
|
return res.send(data);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/logout',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
await userService.logout(req.platform);
|
||||||
|
res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
username: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
await userService.updateUsernameAndPassword(req.body);
|
||||||
|
res.send({ code: 200, message: '更新成功' });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const authInfo = await userService.getUserInfo();
|
||||||
|
res.send({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
username: authInfo.username,
|
||||||
|
avatar: authInfo.avatar,
|
||||||
|
twoFactorActivated: authInfo.twoFactorActivated,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/two-factor/init',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.initTwoFactor();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/two-factor/active',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
code: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.activeTwoFactor(req.body.code);
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/two-factor/deactive',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.deactiveTwoFactor();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/two-factor/login',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
code: Joi.string().required(),
|
||||||
|
username: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.twoFactorLogin(req.body, req);
|
||||||
|
res.send(data);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/login-log',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.getLoginLog();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/notification',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.getNotificationMode();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/notification',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const result = await userService.updateNotificationMode(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/init',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
username: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
await userService.updateUsernameAndPassword(req.body);
|
||||||
|
res.send({ code: 200, message: '更新成功' });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/notification/init',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const result = await userService.updateNotificationMode(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
+12
-10
@@ -1,28 +1,30 @@
|
|||||||
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').default({ expressApp: app });
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
app
|
await require('./loaders/initFile').default();
|
||||||
|
|
||||||
|
await require('./loaders/sentry').default({ expressApp: app });
|
||||||
|
|
||||||
|
await require('./loaders/app').default({ expressApp: 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);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await require('./loaders/server').default({ server });
|
||||||
}
|
}
|
||||||
|
|
||||||
startServer();
|
startServer();
|
||||||
|
|||||||
+65
-30
@@ -1,45 +1,63 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { createRandomString } from './util';
|
||||||
|
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
const envFound = dotenv.config();
|
if (!process.env.QL_DIR) {
|
||||||
const rootPath = path.resolve(__dirname, '../../');
|
// 声明QL_DIR环境变量
|
||||||
const cookieFile = path.join(rootPath, 'config/cookie.sh');
|
let qlHomePath = path.join(__dirname, '../../');
|
||||||
const confFile = path.join(rootPath, 'config/config.sh');
|
// 生产环境
|
||||||
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
|
if (qlHomePath.endsWith('/static/')) {
|
||||||
const crontabFile = path.join(rootPath, 'config/crontab.list');
|
qlHomePath = path.join(qlHomePath, '../');
|
||||||
const confBakDir = path.join(rootPath, 'config/bak/');
|
}
|
||||||
const authConfigFile = path.join(rootPath, 'config/auth.json');
|
process.env.QL_DIR = qlHomePath;
|
||||||
const extraFile = path.join(rootPath, 'config/extra.sh');
|
}
|
||||||
const logPath = path.join(rootPath, 'log/');
|
|
||||||
|
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
|
||||||
|
|
||||||
|
const rootPath = process.env.QL_DIR as string;
|
||||||
|
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||||
|
|
||||||
|
const dataPath = path.join(rootPath, 'data/');
|
||||||
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
|
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 versionFile = path.join(rootPath, 'src/version.ts');
|
||||||
const manualLogPath = path.join(rootPath, 'manual_log/');
|
|
||||||
const cronDbFile = path.join(rootPath, 'db/crontab.db');
|
|
||||||
const cookieDbFile = path.join(rootPath, 'db/cookie.db');
|
|
||||||
const configFound = dotenv.config({ path: confFile });
|
|
||||||
|
|
||||||
if (envFound.error) {
|
if (envFound.error) {
|
||||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configFound.error) {
|
|
||||||
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
port: parseInt(process.env.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),
|
||||||
secret: process.env.SECRET,
|
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
|
||||||
|
secret: process.env.SECRET || createRandomString(16, 32),
|
||||||
logs: {
|
logs: {
|
||||||
level: process.env.LOG_LEVEL || 'silly',
|
level: process.env.LOG_LEVEL || 'silly',
|
||||||
},
|
},
|
||||||
api: {
|
api: {
|
||||||
prefix: '/api',
|
prefix: '/api',
|
||||||
},
|
},
|
||||||
|
rootPath,
|
||||||
configString,
|
configString,
|
||||||
loginFaild,
|
loginFaild,
|
||||||
authError,
|
authError,
|
||||||
@@ -50,14 +68,31 @@ export default {
|
|||||||
crontabFile,
|
crontabFile,
|
||||||
sampleFile,
|
sampleFile,
|
||||||
confFile,
|
confFile,
|
||||||
cookieFile,
|
envFile,
|
||||||
fileMap: {
|
|
||||||
'config.sh': confFile,
|
|
||||||
'crontab.list': crontabFile,
|
|
||||||
'extra.sh': extraFile,
|
|
||||||
},
|
|
||||||
dbPath,
|
dbPath,
|
||||||
cronDbFile,
|
uploadPath,
|
||||||
cookieDbFile,
|
configPath,
|
||||||
manualLogPath,
|
scriptPath,
|
||||||
|
samplePath,
|
||||||
|
blackFileList: [
|
||||||
|
'auth.json',
|
||||||
|
'config.sh.sample',
|
||||||
|
'cookie.sh',
|
||||||
|
'crontab.list',
|
||||||
|
'env.sh',
|
||||||
|
'token.json',
|
||||||
|
],
|
||||||
|
writePathList: [configPath, scriptPath],
|
||||||
|
bakPath,
|
||||||
|
apiWhiteList: [
|
||||||
|
'/api/user/login',
|
||||||
|
'/open/auth/token',
|
||||||
|
'/api/user/two-factor/login',
|
||||||
|
'/api/system',
|
||||||
|
'/api/user/init',
|
||||||
|
'/api/user/notification/init',
|
||||||
|
],
|
||||||
|
versionFile,
|
||||||
|
lastVersionFile,
|
||||||
|
sqliteFile,
|
||||||
};
|
};
|
||||||
|
|||||||
+247
-3
@@ -1,5 +1,8 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import got from 'got';
|
||||||
|
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)) {
|
||||||
@@ -27,7 +30,7 @@ export function getLastModifyFilePath(dir: string) {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPassword(min: number, max: number): string {
|
export function createRandomString(min: number, max: number): string {
|
||||||
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
const english = [
|
const english = [
|
||||||
'a',
|
'a',
|
||||||
@@ -85,7 +88,7 @@ export function createPassword(min: number, max: number): string {
|
|||||||
'Y',
|
'Y',
|
||||||
'Z',
|
'Z',
|
||||||
];
|
];
|
||||||
const special = ['-', '_', '#'];
|
const special = ['-', '_'];
|
||||||
const config = num.concat(english).concat(ENGLISH).concat(special);
|
const config = num.concat(english).concat(ENGLISH).concat(special);
|
||||||
|
|
||||||
const arr = [];
|
const arr = [];
|
||||||
@@ -105,9 +108,250 @@ export function createPassword(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)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return newArr.join('');
|
return newArr.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getToken(req: any) {
|
||||||
|
const { authorization = '' } = req.headers;
|
||||||
|
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
||||||
|
return (authorization as string)
|
||||||
|
.replace('Bearer ', '')
|
||||||
|
.replace('mobile-', '')
|
||||||
|
.replace('desktop-', '');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNetIp(req: any) {
|
||||||
|
const ipArray = [
|
||||||
|
...new Set([
|
||||||
|
...(req.headers['x-real-ip'] || '').split(','),
|
||||||
|
...(req.headers['x-forwarded-for'] || '').split(','),
|
||||||
|
req.ip,
|
||||||
|
...req.ips,
|
||||||
|
req.socket.remoteAddress,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
let ip = ipArray[0];
|
||||||
|
|
||||||
|
if (ipArray.length > 1) {
|
||||||
|
for (let i = 0; i < ipArray.length; i++) {
|
||||||
|
const ipNumArray = ipArray[i].split('.');
|
||||||
|
const tmp = ipNumArray[0] + '.' + ipNumArray[1];
|
||||||
|
if (
|
||||||
|
tmp === '192.168' ||
|
||||||
|
(ipNumArray[0] === '172' &&
|
||||||
|
ipNumArray[1] >= 16 &&
|
||||||
|
ipNumArray[1] <= 32) ||
|
||||||
|
tmp === '10.7' ||
|
||||||
|
tmp === '127.0'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ip = ipArray[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
|
||||||
|
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
||||||
|
ip = '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const baiduApi = got
|
||||||
|
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
||||||
|
.text();
|
||||||
|
const ipApi = got
|
||||||
|
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
|
||||||
|
timeout: 10000,
|
||||||
|
retry: 0,
|
||||||
|
})
|
||||||
|
.buffer();
|
||||||
|
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
|
||||||
|
|
||||||
|
const ipRegx = /.*IP :(.*)\n/;
|
||||||
|
const addrRegx = /.*数据二 :(.*)\n/;
|
||||||
|
if (data && ipRegx.test(data) && addrRegx.test(data)) {
|
||||||
|
const ip = data.match(ipRegx)[1];
|
||||||
|
const addr = data.match(addrRegx)[1];
|
||||||
|
return { address: addr, ip };
|
||||||
|
} else if (ipApiBody) {
|
||||||
|
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
|
||||||
|
return { address: `${addr}`, ip };
|
||||||
|
} else {
|
||||||
|
return { address: `获取失败`, ip };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { address: `获取失败`, ip };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
||||||
|
const ua = userAgent.toLowerCase();
|
||||||
|
const testUa = (regexp: RegExp) => regexp.test(ua);
|
||||||
|
const testVs = (regexp: RegExp) =>
|
||||||
|
(ua.match(regexp) || [])
|
||||||
|
.toString()
|
||||||
|
.replace(/[^0-9|_.]/g, '')
|
||||||
|
.replace(/_/g, '.');
|
||||||
|
|
||||||
|
// 系统
|
||||||
|
let system = 'unknow';
|
||||||
|
if (testUa(/windows|win32|win64|wow32|wow64/g)) {
|
||||||
|
system = 'windows'; // windows系统
|
||||||
|
} else if (testUa(/macintosh|macintel/g)) {
|
||||||
|
system = 'macos'; // macos系统
|
||||||
|
} else if (testUa(/x11/g)) {
|
||||||
|
system = 'linux'; // linux系统
|
||||||
|
} else if (testUa(/android|adr/g)) {
|
||||||
|
system = 'android'; // android系统
|
||||||
|
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
||||||
|
system = 'ios'; // ios系统
|
||||||
|
}
|
||||||
|
|
||||||
|
let platform = 'desktop';
|
||||||
|
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
||||||
|
platform = 'desktop';
|
||||||
|
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
|
||||||
|
platform = 'mobile';
|
||||||
|
}
|
||||||
|
|
||||||
|
return platform as 'mobile' | 'desktop';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fileExist(file: any) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
fs.accessSync(file);
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
fnList: Array<() => Promise<any>> = [],
|
||||||
|
max = 5,
|
||||||
|
) {
|
||||||
|
if (!fnList.length) return;
|
||||||
|
|
||||||
|
const replyList: any[] = []; // 收集任务执行结果
|
||||||
|
const startTime = new Date().getTime(); // 记录任务执行开始时间
|
||||||
|
|
||||||
|
// 任务执行程序
|
||||||
|
const schedule = async (index: number) => {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const fn = fnList[index];
|
||||||
|
if (!fn) return resolve(null);
|
||||||
|
|
||||||
|
// 执行当前异步任务
|
||||||
|
const reply = await fn();
|
||||||
|
replyList[index] = reply;
|
||||||
|
|
||||||
|
// 执行完当前任务后,继续执行任务池的剩余任务
|
||||||
|
await schedule(index + max);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 任务池执行程序
|
||||||
|
const scheduleList = new Array(max)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => schedule(index));
|
||||||
|
|
||||||
|
// 使用 Promise.all 批量执行
|
||||||
|
const r = await Promise.all(scheduleList);
|
||||||
|
const cost = (new Date().getTime() - startTime) / 1000;
|
||||||
|
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { sequelize } from '.';
|
||||||
|
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||||
|
|
||||||
|
export class AuthInfo {
|
||||||
|
ip?: string;
|
||||||
|
type: AuthDataType;
|
||||||
|
info?: any;
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
constructor(options: AuthInfo) {
|
||||||
|
this.ip = options.ip;
|
||||||
|
this.info = options.info;
|
||||||
|
this.type = options.type;
|
||||||
|
this.id = options.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LoginStatus {
|
||||||
|
'success',
|
||||||
|
'fail',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthDataType {
|
||||||
|
'loginLog' = 'loginLog',
|
||||||
|
'authToken' = 'authToken',
|
||||||
|
'notification' = 'notification',
|
||||||
|
'removeLogFrequency' = 'removeLogFrequency',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
|
||||||
|
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
|
||||||
|
ip: DataTypes.STRING,
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
info: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
export class Cookie {
|
|
||||||
value?: string;
|
|
||||||
timestamp?: string;
|
|
||||||
created?: number;
|
|
||||||
_id?: string;
|
|
||||||
status?: CookieStatus;
|
|
||||||
position?: number;
|
|
||||||
|
|
||||||
constructor(options: Cookie) {
|
|
||||||
this.value = options.value;
|
|
||||||
this._id = options._id;
|
|
||||||
this.created = options.created || new Date().valueOf();
|
|
||||||
this.status = options.status || CookieStatus.noacquired;
|
|
||||||
this.timestamp = new Date().toString();
|
|
||||||
this.position = options.position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CookieStatus {
|
|
||||||
'noacquired',
|
|
||||||
'normal',
|
|
||||||
'disabled',
|
|
||||||
'invalid',
|
|
||||||
'abnormal',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initCookiePosition = 9999999999;
|
|
||||||
+47
-6
@@ -1,28 +1,42 @@
|
|||||||
|
import { sequelize } from '.';
|
||||||
|
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;
|
||||||
created?: number;
|
|
||||||
saved?: boolean;
|
saved?: boolean;
|
||||||
_id?: string;
|
id?: number;
|
||||||
status?: CrontabStatus;
|
status?: CrontabStatus;
|
||||||
isSystem?: 1 | 0;
|
isSystem?: 1 | 0;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
isDisabled?: 1 | 0;
|
isDisabled?: 1 | 0;
|
||||||
|
log_path?: string;
|
||||||
|
isPinned?: 1 | 0;
|
||||||
|
labels?: string[];
|
||||||
|
last_running_time?: number;
|
||||||
|
last_execution_time?: number;
|
||||||
|
|
||||||
constructor(options: Crontab) {
|
constructor(options: Crontab) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.command = options.command;
|
this.command = options.command;
|
||||||
this.schedule = options.schedule;
|
this.schedule = options.schedule;
|
||||||
this.saved = options.saved;
|
this.saved = options.saved;
|
||||||
this._id = options._id;
|
this.id = options.id;
|
||||||
this.created = options.created;
|
this.status =
|
||||||
this.status = options.status || CrontabStatus.idle;
|
options.status && CrontabStatus[options.status]
|
||||||
|
? options.status
|
||||||
|
: CrontabStatus.idle;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.isSystem = options.isSystem || 0;
|
this.isSystem = options.isSystem || 0;
|
||||||
this.pid = options.pid;
|
this.pid = options.pid;
|
||||||
this.isDisabled = options.isDisabled || 0;
|
this.isDisabled = options.isDisabled || 0;
|
||||||
|
this.log_path = options.log_path || '';
|
||||||
|
this.isPinned = options.isPinned || 0;
|
||||||
|
this.labels = options.labels || [];
|
||||||
|
this.last_running_time = options.last_running_time || 0;
|
||||||
|
this.last_execution_time = options.last_execution_time || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,3 +46,30 @@ export enum CrontabStatus {
|
|||||||
'disabled',
|
'disabled',
|
||||||
'queued',
|
'queued',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
||||||
|
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||||
|
name: {
|
||||||
|
unique: 'compositeIndex',
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
unique: 'compositeIndex',
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
schedule: {
|
||||||
|
unique: 'compositeIndex',
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
timestamp: DataTypes.STRING,
|
||||||
|
saved: DataTypes.BOOLEAN,
|
||||||
|
status: DataTypes.NUMBER,
|
||||||
|
isSystem: DataTypes.NUMBER,
|
||||||
|
pid: DataTypes.NUMBER,
|
||||||
|
isDisabled: DataTypes.NUMBER,
|
||||||
|
isPinned: DataTypes.NUMBER,
|
||||||
|
log_path: DataTypes.STRING,
|
||||||
|
labels: DataTypes.JSON,
|
||||||
|
last_running_time: DataTypes.NUMBER,
|
||||||
|
last_execution_time: DataTypes.NUMBER,
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { sequelize } from '.';
|
||||||
|
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||||
|
|
||||||
|
export class Dependence {
|
||||||
|
timestamp?: string;
|
||||||
|
id?: number;
|
||||||
|
status?: DependenceStatus;
|
||||||
|
type?: DependenceTypes;
|
||||||
|
name?: number;
|
||||||
|
log?: string[];
|
||||||
|
remark?: string;
|
||||||
|
|
||||||
|
constructor(options: Dependence) {
|
||||||
|
this.id = options.id;
|
||||||
|
this.status = options.status || DependenceStatus.installing;
|
||||||
|
this.type = options.type || DependenceTypes.nodejs;
|
||||||
|
this.timestamp = new Date().toString();
|
||||||
|
this.name = options.name;
|
||||||
|
this.log = options.log || [];
|
||||||
|
this.remark = options.remark || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DependenceStatus {
|
||||||
|
'installing',
|
||||||
|
'installed',
|
||||||
|
'installFailed',
|
||||||
|
'removing',
|
||||||
|
'removed',
|
||||||
|
'removeFailed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DependenceTypes {
|
||||||
|
'nodejs',
|
||||||
|
'python3',
|
||||||
|
'linux',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum InstallDependenceCommandTypes {
|
||||||
|
'pnpm add -g',
|
||||||
|
'pip3 install',
|
||||||
|
'apk add',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum unInstallDependenceCommandTypes {
|
||||||
|
'pnpm remove -g',
|
||||||
|
'pip3 uninstall -y',
|
||||||
|
'apk del',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DependenceInstance
|
||||||
|
extends Model<Dependence, Dependence>,
|
||||||
|
Dependence {}
|
||||||
|
export const DependenceModel = sequelize.define<DependenceInstance>(
|
||||||
|
'Dependence',
|
||||||
|
{
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
timestamp: DataTypes.STRING,
|
||||||
|
status: DataTypes.NUMBER,
|
||||||
|
log: DataTypes.JSON,
|
||||||
|
remark: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { sequelize } from '.';
|
||||||
|
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||||
|
|
||||||
|
export class Env {
|
||||||
|
value?: string;
|
||||||
|
timestamp?: string;
|
||||||
|
id?: number;
|
||||||
|
status?: EnvStatus;
|
||||||
|
position?: number;
|
||||||
|
name?: string;
|
||||||
|
remarks?: string;
|
||||||
|
|
||||||
|
constructor(options: Env) {
|
||||||
|
this.value = options.value;
|
||||||
|
this.id = options.id;
|
||||||
|
this.status = options.status || EnvStatus.normal;
|
||||||
|
this.timestamp = new Date().toString();
|
||||||
|
this.position = options.position;
|
||||||
|
this.name = options.name;
|
||||||
|
this.remarks = options.remarks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EnvStatus {
|
||||||
|
'normal',
|
||||||
|
'disabled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initEnvPosition = 9999999999;
|
||||||
|
|
||||||
|
interface EnvInstance extends Model<Env, Env>, Env {}
|
||||||
|
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||||
|
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||||
|
timestamp: DataTypes.STRING,
|
||||||
|
status: DataTypes.NUMBER,
|
||||||
|
position: DataTypes.NUMBER,
|
||||||
|
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||||
|
remarks: DataTypes.STRING,
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
import config from '../config/index';
|
||||||
|
|
||||||
|
export const sequelize = new Sequelize({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
storage: `${config.dbPath}database.sqlite`,
|
||||||
|
logging: false,
|
||||||
|
pool: {
|
||||||
|
max: 6,
|
||||||
|
min: 0,
|
||||||
|
idle: 30000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResponseType<T> = { code: number; data?: T; message?: string };
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
export enum NotificationMode {
|
||||||
|
'gotify' = 'gotify',
|
||||||
|
'goCqHttpBot' = 'goCqHttpBot',
|
||||||
|
'serverChan' = 'serverChan',
|
||||||
|
'pushDeer' = 'pushDeer',
|
||||||
|
'bark' = 'bark',
|
||||||
|
'telegramBot' = 'telegramBot',
|
||||||
|
'dingtalkBot' = 'dingtalkBot',
|
||||||
|
'weWorkBot' = 'weWorkBot',
|
||||||
|
'weWorkApp' = 'weWorkApp',
|
||||||
|
'iGot' = 'iGot',
|
||||||
|
'pushPlus' = 'pushPlus',
|
||||||
|
'email' = 'email',
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class NotificationBaseInfo {
|
||||||
|
public type!: NotificationMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GotifyNotification extends NotificationBaseInfo {
|
||||||
|
public gotifyUrl = '';
|
||||||
|
public gotifyToken = '';
|
||||||
|
public gotifyPriority = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GoCqHttpBotNotification extends NotificationBaseInfo {
|
||||||
|
public goCqHttpBotUrl = '';
|
||||||
|
public goCqHttpBotToken = '';
|
||||||
|
public goCqHttpBotQq = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServerChanNotification extends NotificationBaseInfo {
|
||||||
|
public serverChanKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PushDeerNotification extends NotificationBaseInfo {
|
||||||
|
public pushDeerKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BarkNotification extends NotificationBaseInfo {
|
||||||
|
public barkPush = '';
|
||||||
|
public barkIcon = 'http://qn.whyour.cn/logo.png';
|
||||||
|
public barkSound = '';
|
||||||
|
public barkGroup = 'qinglong';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||||
|
public telegramBotToken = '';
|
||||||
|
public telegramBotUserId = '';
|
||||||
|
public telegramBotProxyHost = '';
|
||||||
|
public telegramBotProxyPort = '';
|
||||||
|
public telegramBotProxyAuth = '';
|
||||||
|
public telegramBotApiHost = 'api.telegram.org';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DingtalkBotNotification extends NotificationBaseInfo {
|
||||||
|
public dingtalkBotToken = '';
|
||||||
|
public dingtalkBotSecret = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WeWorkBotNotification extends NotificationBaseInfo {
|
||||||
|
public weWorkBotKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WeWorkAppNotification extends NotificationBaseInfo {
|
||||||
|
public weWorkAppKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IGotNotification extends NotificationBaseInfo {
|
||||||
|
public iGotPushKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PushPlusNotification extends NotificationBaseInfo {
|
||||||
|
public pushPlusToken = '';
|
||||||
|
public pushPlusUser = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailNotification extends NotificationBaseInfo {
|
||||||
|
public emailService: string = '';
|
||||||
|
public emailUser: string = '';
|
||||||
|
public emailPass: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationInfo
|
||||||
|
extends GoCqHttpBotNotification,
|
||||||
|
GotifyNotification,
|
||||||
|
ServerChanNotification,
|
||||||
|
PushDeerNotification,
|
||||||
|
BarkNotification,
|
||||||
|
TelegramBotNotification,
|
||||||
|
DingtalkBotNotification,
|
||||||
|
WeWorkBotNotification,
|
||||||
|
WeWorkAppNotification,
|
||||||
|
IGotNotification,
|
||||||
|
PushPlusNotification,
|
||||||
|
EmailNotification {}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { sequelize } from '.';
|
||||||
|
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||||
|
|
||||||
|
export class App {
|
||||||
|
name: string;
|
||||||
|
scopes: AppScope[];
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
tokens?: AppToken[];
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
constructor(options: App) {
|
||||||
|
this.name = options.name;
|
||||||
|
this.scopes = options.scopes;
|
||||||
|
this.client_id = options.client_id;
|
||||||
|
this.client_secret = options.client_secret;
|
||||||
|
this.id = options.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppToken {
|
||||||
|
value: string;
|
||||||
|
type?: 'Bearer';
|
||||||
|
expiration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
||||||
|
|
||||||
|
export enum CrontabStatus {
|
||||||
|
'running',
|
||||||
|
'idle',
|
||||||
|
'disabled',
|
||||||
|
'queued',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppInstance extends Model<App, App>, App {}
|
||||||
|
export const AppModel = sequelize.define<AppInstance>('App', {
|
||||||
|
name: { type: DataTypes.STRING, unique: 'name' },
|
||||||
|
scopes: DataTypes.JSON,
|
||||||
|
client_id: DataTypes.STRING,
|
||||||
|
client_secret: DataTypes.STRING,
|
||||||
|
tokens: DataTypes.JSON,
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export class SockMessage {
|
||||||
|
message?: string;
|
||||||
|
type?: SockMessageType;
|
||||||
|
references?: number[];
|
||||||
|
|
||||||
|
constructor(options: SockMessage) {
|
||||||
|
this.type = options.type;
|
||||||
|
this.message = options.message;
|
||||||
|
this.references = options.references;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SockMessageType =
|
||||||
|
| 'ping'
|
||||||
|
| 'installDependence'
|
||||||
|
| 'uninstallDependence'
|
||||||
|
| 'updateSystemVersion'
|
||||||
|
| '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' },
|
||||||
|
},
|
||||||
|
);
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
interface Request {
|
||||||
|
platform: 'desktop' | 'mobile';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import expressLoader from './express';
|
||||||
|
import depInjectorLoader from './depInjector';
|
||||||
|
import Logger from './logger';
|
||||||
|
import initData from './initData';
|
||||||
|
import { Application } from 'express';
|
||||||
|
import linkDeps from './deps';
|
||||||
|
import initTask from './initTask';
|
||||||
|
|
||||||
|
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||||
|
await depInjectorLoader();
|
||||||
|
Logger.info('✌️ Dependency Injector loaded');
|
||||||
|
|
||||||
|
await expressLoader({ app: expressApp });
|
||||||
|
Logger.info('✌️ Express loaded');
|
||||||
|
|
||||||
|
await initData();
|
||||||
|
Logger.info('✌️ init data loaded');
|
||||||
|
|
||||||
|
await linkDeps();
|
||||||
|
Logger.info('✌️ link deps loaded');
|
||||||
|
|
||||||
|
initTask();
|
||||||
|
Logger.info('✌️ init task loaded');
|
||||||
|
};
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import Logger from './logger';
|
||||||
|
import path from 'path';
|
||||||
|
import DataStore from 'nedb';
|
||||||
|
import { EnvModel } from '../data/env';
|
||||||
|
import { CrontabModel } from '../data/cron';
|
||||||
|
import { DependenceModel } from '../data/dependence';
|
||||||
|
import { AppModel } from '../data/open';
|
||||||
|
import { AuthModel } from '../data/auth';
|
||||||
|
import { sequelize } from '../data';
|
||||||
|
import { fileExist } from '../config/util';
|
||||||
|
import { SubscriptionModel } from '../data/subscription';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
try {
|
||||||
|
await CrontabModel.sync();
|
||||||
|
await DependenceModel.sync();
|
||||||
|
await AppModel.sync();
|
||||||
|
await AuthModel.sync();
|
||||||
|
await EnvModel.sync();
|
||||||
|
await SubscriptionModel.sync();
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const queryInterface = sequelize.getQueryInterface();
|
||||||
|
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
|
||||||
|
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
|
||||||
|
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
|
||||||
|
// } catch (error) { }
|
||||||
|
|
||||||
|
// 2.10-2.11 升级
|
||||||
|
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
||||||
|
const envDbFile = path.join(config.rootPath, 'db/env.db');
|
||||||
|
const appDbFile = path.join(config.rootPath, 'db/app.db');
|
||||||
|
const authDbFile = path.join(config.rootPath, 'db/auth.db');
|
||||||
|
const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
|
||||||
|
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 dependenceCount = await DependenceModel.count();
|
||||||
|
const envCount = await EnvModel.count();
|
||||||
|
const appCount = await AppModel.count();
|
||||||
|
const authCount = await AuthModel.count();
|
||||||
|
if (crondbExist && cronCount === 0) {
|
||||||
|
const cronDb = new DataStore({
|
||||||
|
filename: cronDbFile,
|
||||||
|
autoload: true,
|
||||||
|
});
|
||||||
|
cronDb.persistence.compactDatafile();
|
||||||
|
cronDb.find({}).exec(async (err, docs) => {
|
||||||
|
await CrontabModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dependenceDbExist && dependenceCount === 0) {
|
||||||
|
const dependenceDb = new DataStore({
|
||||||
|
filename: dependenceDbFile,
|
||||||
|
autoload: true,
|
||||||
|
});
|
||||||
|
dependenceDb.persistence.compactDatafile();
|
||||||
|
dependenceDb.find({}).exec(async (err, docs) => {
|
||||||
|
await DependenceModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envDbExist && envCount === 0) {
|
||||||
|
const envDb = new DataStore({
|
||||||
|
filename: envDbFile,
|
||||||
|
autoload: true,
|
||||||
|
});
|
||||||
|
envDb.persistence.compactDatafile();
|
||||||
|
envDb.find({}).exec(async (err, docs) => {
|
||||||
|
await EnvModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appDbExist && appCount === 0) {
|
||||||
|
const appDb = new DataStore({
|
||||||
|
filename: appDbFile,
|
||||||
|
autoload: true,
|
||||||
|
});
|
||||||
|
appDb.persistence.compactDatafile();
|
||||||
|
appDb.find({}).exec(async (err, docs) => {
|
||||||
|
await AppModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authDbExist && authCount === 0) {
|
||||||
|
const authDb = new DataStore({
|
||||||
|
filename: authDbFile,
|
||||||
|
autoload: true,
|
||||||
|
});
|
||||||
|
authDb.persistence.compactDatafile();
|
||||||
|
authDb.find({}).exec(async (err, docs) => {
|
||||||
|
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info('✌️ DB loaded');
|
||||||
|
} catch (error) {
|
||||||
|
Logger.info('✌️ DB load failed');
|
||||||
|
Logger.info(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import chokidar from 'chokidar';
|
||||||
|
import config from '../config/index';
|
||||||
|
import { promiseExec } from '../config/util';
|
||||||
|
|
||||||
|
function linkToNodeModule(src: string, dst?: string) {
|
||||||
|
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
||||||
|
const source = path.join(config.rootPath, src);
|
||||||
|
|
||||||
|
fs.lstat(target, (err, stat) => {
|
||||||
|
if (!stat) {
|
||||||
|
fs.symlink(source, target, 'dir', (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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') => {
|
||||||
|
await linkCommand();
|
||||||
|
linkToNodeModule(src);
|
||||||
|
|
||||||
|
const source = path.join(config.rootPath, src);
|
||||||
|
const watcher = chokidar.watch(source, {
|
||||||
|
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
||||||
|
persistent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher
|
||||||
|
.on('add', (path) => linkToNodeModule(src))
|
||||||
|
.on('change', (path) => linkToNodeModule(src));
|
||||||
|
};
|
||||||
+123
-9
@@ -1,38 +1,125 @@
|
|||||||
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 { getPlatform, getToken } from '../config/util';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import OpenService from '../services/open';
|
||||||
|
import rewrite from 'express-urlrewrite';
|
||||||
|
import UserService from '../services/user';
|
||||||
|
import handler from 'serve-handler';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { EnvModel } from '../data/env';
|
||||||
|
import { errors } from 'celebrate';
|
||||||
|
|
||||||
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) => {
|
||||||
|
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
return handler(req, res, {
|
||||||
|
public: 'static/dist',
|
||||||
|
rewrites: [{ source: '**', destination: '/index.html' }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({
|
jwt({
|
||||||
path: ['/api/login'],
|
secret: config.secret as string,
|
||||||
|
algorithms: ['HS384'],
|
||||||
|
}).unless({
|
||||||
|
path: [...config.apiWhiteList, /^\/open\//],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (req.url && req.url.includes('/api/login')) {
|
app.use((req: Request, res, next) => {
|
||||||
|
if (!req.headers) {
|
||||||
|
req.platform = 'desktop';
|
||||||
|
} else {
|
||||||
|
const platform = getPlatform(req.headers['user-agent'] || '');
|
||||||
|
req.platform = platform;
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
const headerToken = getToken(req);
|
||||||
|
if (req.path.startsWith('/open/')) {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const doc = await openService.findTokenByValue(headerToken);
|
||||||
|
if (doc && doc.tokens && doc.tokens.length > 0) {
|
||||||
|
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
||||||
|
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
||||||
|
const key = keyMatch && keyMatch[1];
|
||||||
|
if (
|
||||||
|
doc.scopes.includes(key as any) &&
|
||||||
|
currentToken &&
|
||||||
|
currentToken.expiration >= Math.round(Date.now() / 1000)
|
||||||
|
) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const originPath = `${req.baseUrl}${req.path === '/' ? '' : req.path}`;
|
||||||
|
if (
|
||||||
|
!headerToken &&
|
||||||
|
originPath &&
|
||||||
|
config.apiWhiteList.includes(originPath) &&
|
||||||
|
originPath !== '/api/crons/status'
|
||||||
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
const authHeader = req.headers.authorization;
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const { token } = JSON.parse(data);
|
const { token = '', tokens = {} } = JSON.parse(data);
|
||||||
if (token && authHeader.includes(token)) {
|
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const err: any = new Error('UnauthorizedError');
|
const err: any = new Error('UnauthorizedError');
|
||||||
err['status'] = 401;
|
err.status = 401;
|
||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const authInfo = await userService.getUserInfo();
|
||||||
|
const envCount = await EnvModel.count();
|
||||||
|
|
||||||
|
let isInitialized = true;
|
||||||
|
if (
|
||||||
|
Object.keys(authInfo).length === 2 &&
|
||||||
|
authInfo.username === 'admin' &&
|
||||||
|
authInfo.password === 'admin' &&
|
||||||
|
envCount === 0
|
||||||
|
) {
|
||||||
|
isInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInitialized) {
|
||||||
|
return res.send({ code: 450, message: '未知错误' });
|
||||||
|
} else {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(rewrite('/open/*', '/api/$1'));
|
||||||
app.use(config.api.prefix, routes());
|
app.use(config.api.prefix, routes());
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
@@ -41,6 +128,8 @@ export default ({ app }: { app: Application }) => {
|
|||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(errors());
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
err: Error & { status: number },
|
err: Error & { status: number },
|
||||||
@@ -58,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 },
|
||||||
@@ -65,6 +177,8 @@ export default ({ app }: { app: Application }) => {
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
) => {
|
) => {
|
||||||
|
Sentry.captureException(err);
|
||||||
|
|
||||||
res.status(err.status || 500);
|
res.status(err.status || 500);
|
||||||
res.json({
|
res.json({
|
||||||
code: err.status || 500,
|
code: err.status || 500,
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import expressLoader from './express';
|
|
||||||
import dependencyInjectorLoader from './dependencyInjector';
|
|
||||||
import Logger from './logger';
|
|
||||||
import initData from './initData';
|
|
||||||
|
|
||||||
export default async ({ expressApp }: { expressApp: any }) => {
|
|
||||||
Logger.info('✌️ DB loaded and connected!');
|
|
||||||
|
|
||||||
await dependencyInjectorLoader({
|
|
||||||
models: [],
|
|
||||||
});
|
|
||||||
Logger.info('✌️ Dependency Injector loaded');
|
|
||||||
|
|
||||||
await expressLoader({ app: expressApp });
|
|
||||||
Logger.info('✌️ Express loaded');
|
|
||||||
|
|
||||||
await initData();
|
|
||||||
Logger.info('✌️ init data loaded');
|
|
||||||
};
|
|
||||||
+99
-99
@@ -1,117 +1,117 @@
|
|||||||
|
import DependenceService from '../services/dependence';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Crontab, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
import CronService from '../services/cron';
|
import CronService from '../services/cron';
|
||||||
import CookieService from '../services/cookie';
|
import EnvService from '../services/env';
|
||||||
|
import _ from 'lodash';
|
||||||
const initData = [
|
import { DependenceModel } from '../data/dependence';
|
||||||
{
|
import { Op } from 'sequelize';
|
||||||
name: '更新面板',
|
import config from '../config';
|
||||||
command: `ql update`,
|
|
||||||
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
|
|
||||||
6,
|
|
||||||
1,
|
|
||||||
).toString()} * * *`,
|
|
||||||
status: CrontabStatus.disabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '删除日志',
|
|
||||||
command: 'ql rmlog 7',
|
|
||||||
schedule: '30 7 */7 * *',
|
|
||||||
status: CrontabStatus.idle,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '互助码',
|
|
||||||
command: 'ql code',
|
|
||||||
schedule: '30 7 * * *',
|
|
||||||
status: CrontabStatus.idle,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const cookieService = Container.get(CookieService);
|
const envService = Container.get(EnvService);
|
||||||
const cronDb = cronService.getDb();
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
|
||||||
cronDb.count({}, async (err, count) => {
|
// 初始化更新所有任务状态为空闲
|
||||||
if (count === 0) {
|
await CrontabModel.update(
|
||||||
const data = initData.map((x) => {
|
{ status: CrontabStatus.idle },
|
||||||
const tab = new Crontab(x);
|
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
|
||||||
tab.created = new Date().valueOf();
|
);
|
||||||
tab.saved = false;
|
|
||||||
if (tab.name === '更新面板') {
|
|
||||||
tab.isSystem = 1;
|
|
||||||
} else {
|
|
||||||
tab.isSystem = 0;
|
|
||||||
}
|
|
||||||
return tab;
|
|
||||||
});
|
|
||||||
cronDb.insert(data);
|
|
||||||
await cronService.autosave_crontab();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// patch更新面板任务状态
|
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||||
cronDb.find({ name: '更新面板' }).exec((err, docs) => {
|
DependenceModel.findAll({
|
||||||
const doc = docs[0];
|
where: {},
|
||||||
if (doc && doc.status === CrontabStatus.running) {
|
order: [['type', 'DESC']],
|
||||||
cronDb.update(
|
raw: true,
|
||||||
{ name: '更新面板' },
|
}).then(async (docs) => {
|
||||||
{ $set: { status: CrontabStatus.idle } },
|
const groups = _.groupBy(docs, 'type');
|
||||||
);
|
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
|
||||||
|
for (const key of keys) {
|
||||||
|
const group = groups[key];
|
||||||
|
const depIds = group.map((x) => x.id);
|
||||||
|
await dependenceService.reInstall(depIds as number[]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化时执行一次所有的ql repo 任务
|
// 初始化时执行一次所有的ql repo 任务
|
||||||
cronDb
|
CrontabModel.findAll({
|
||||||
.find({
|
where: {
|
||||||
command: /ql (repo|raw)/,
|
isDisabled: { [Op.ne]: 1 },
|
||||||
})
|
command: {
|
||||||
.exec((err, docs) => {
|
[Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%ql raw%` }],
|
||||||
for (let i = 0; i < docs.length; i++) {
|
},
|
||||||
const doc = docs[i];
|
},
|
||||||
if (doc && doc.isDisabled !== 1) {
|
}).then((docs) => {
|
||||||
exec(doc.command);
|
for (let i = 0; i < docs.length; i++) {
|
||||||
|
const doc = docs[i];
|
||||||
|
if (doc) {
|
||||||
|
exec(doc.command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新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 } },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
// patch 禁用状态字段改变
|
|
||||||
cronDb
|
|
||||||
.find({
|
|
||||||
status: CrontabStatus.disabled,
|
|
||||||
})
|
|
||||||
.exec((err, docs) => {
|
|
||||||
if (docs.length > 0) {
|
|
||||||
const ids = docs.map((x) => x._id);
|
|
||||||
cronDb.update(
|
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ $set: { status: CrontabStatus.idle, isDisabled: 1 } },
|
|
||||||
{ multi: true },
|
|
||||||
(err) => {
|
|
||||||
cronService.autosave_crontab();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化保存一次ck和定时任务数据
|
// 初始化保存一次ck和定时任务数据
|
||||||
await cronService.autosave_crontab();
|
await cronService.autosave_crontab();
|
||||||
await cookieService.set_cookies();
|
await envService.set_envs();
|
||||||
};
|
};
|
||||||
|
|
||||||
function randomSchedule(from: number, to: number) {
|
|
||||||
const result = [];
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import Logger from './logger';
|
||||||
|
import { fileExist } from '../config/util';
|
||||||
|
|
||||||
|
const rootPath = process.env.QL_DIR as string;;
|
||||||
|
const dataPath = path.join(rootPath, 'data/');
|
||||||
|
const configPath = path.join(dataPath, 'config/');
|
||||||
|
const scriptPath = path.join(dataPath, 'scripts/');
|
||||||
|
const logPath = path.join(dataPath, 'log/');
|
||||||
|
const uploadPath = path.join(dataPath, 'upload/');
|
||||||
|
const bakPath = path.join(dataPath, 'bak/');
|
||||||
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
|
const 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 () => {
|
||||||
|
const authFileExist = await fileExist(authConfigFile);
|
||||||
|
const confFileExist = await fileExist(confFile);
|
||||||
|
const scriptDirExist = await fileExist(scriptPath);
|
||||||
|
const logDirExist = await fileExist(logPath);
|
||||||
|
const configDirExist = await fileExist(configPath);
|
||||||
|
const uploadDirExist = await fileExist(uploadPath);
|
||||||
|
const sshDirExist = await fileExist(sshPath);
|
||||||
|
const bakDirExist = await fileExist(bakPath);
|
||||||
|
|
||||||
|
if (!configDirExist) {
|
||||||
|
fs.mkdirSync(configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authFileExist) {
|
||||||
|
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confFileExist) {
|
||||||
|
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scriptDirExist) {
|
||||||
|
fs.mkdirSync(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!logDirExist) {
|
||||||
|
fs.mkdirSync(logPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uploadDirExist) {
|
||||||
|
fs.mkdirSync(uploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sshDirExist) {
|
||||||
|
fs.mkdirSync(sshPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bakDirExist) {
|
||||||
|
fs.mkdirSync(bakPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
dotenv.config({ path: confFile });
|
||||||
|
|
||||||
|
Logger.info('✌️ Init file down');
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Container } from 'typedi';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import SystemService from '../services/system';
|
||||||
|
import ScheduleService from '../services/schedule';
|
||||||
|
import SubscriptionService from '../services/subscription';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const scheduleService = Container.get(ScheduleService);
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
|
||||||
|
// 运行删除日志任务
|
||||||
|
const data = await systemService.getLogRemoveFrequency();
|
||||||
|
if (data && data.info && data.info.frequency) {
|
||||||
|
const cron = {
|
||||||
|
id: data.id,
|
||||||
|
name: '删除日志',
|
||||||
|
command: `ql rmlog ${data.info.frequency}`,
|
||||||
|
};
|
||||||
|
await 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,21 @@
|
|||||||
|
import { Application } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import * as Tracing from '@sentry/tracing';
|
||||||
|
import Logger from './logger';
|
||||||
|
|
||||||
|
export default ({ expressApp }: { expressApp: Application }) => {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
||||||
|
integrations: [
|
||||||
|
new Sentry.Integrations.Http({ tracing: true }),
|
||||||
|
new Tracing.Integrations.Express({ app: expressApp }),
|
||||||
|
],
|
||||||
|
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expressApp.use(Sentry.Handlers.requestHandler());
|
||||||
|
expressApp.use(Sentry.Handlers.tracingHandler());
|
||||||
|
|
||||||
|
Logger.info('✌️ Sentry loaded');
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Server } from 'http';
|
||||||
|
import Logger from './logger';
|
||||||
|
import Sock from './sock';
|
||||||
|
|
||||||
|
export default async ({ server }: { server: Server }) => {
|
||||||
|
await Sock({ server });
|
||||||
|
Logger.info('✌️ Sock loaded');
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
Logger.info('✌️ Server need close');
|
||||||
|
server.close(() => {
|
||||||
|
Logger.info('✌️ Server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import sockJs from 'sockjs';
|
||||||
|
import { Server } from 'http';
|
||||||
|
import Logger from './logger';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import SockService from '../services/sock';
|
||||||
|
import config from '../config/index';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { getPlatform } from '../config/util';
|
||||||
|
|
||||||
|
export default async ({ server }: { server: Server }) => {
|
||||||
|
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||||
|
const sockService = Container.get(SockService);
|
||||||
|
|
||||||
|
echo.on('connection', (conn) => {
|
||||||
|
if (!conn.headers || !conn.url || !conn.pathname) {
|
||||||
|
conn.close('404');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
|
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||||
|
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||||
|
if (data) {
|
||||||
|
const { token = '', tokens = {} } = JSON.parse(data);
|
||||||
|
if (headerToken === token || tokens[platform] === headerToken) {
|
||||||
|
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
||||||
|
sockService.addClient(conn);
|
||||||
|
|
||||||
|
conn.on('data', (message) => {
|
||||||
|
conn.write(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on('close', function () {
|
||||||
|
sockService.removeClient(conn);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
conn.write(JSON.stringify({ type: 'ping', message: 'whyour' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.close('404');
|
||||||
|
});
|
||||||
|
|
||||||
|
echo.installHandlers(server);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
+13
-18
@@ -2,30 +2,18 @@ import schedule from 'node-schedule';
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import { Container } from 'typedi';
|
import { CrontabModel, CrontabStatus } from './data/cron';
|
||||||
import CronService from './services/cron';
|
|
||||||
import { CrontabStatus } from './data/cron';
|
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const cronService = Container.get(CronService);
|
CrontabModel.findAll({ where: {} })
|
||||||
const cronDb = cronService.getDb();
|
.then((docs) => {
|
||||||
|
|
||||||
cronDb
|
|
||||||
.find({})
|
|
||||||
.sort({ created: 1 })
|
|
||||||
.exec((err, docs) => {
|
|
||||||
if (err) {
|
|
||||||
Logger.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (docs && docs.length > 0) {
|
if (docs && docs.length > 0) {
|
||||||
for (let i = 0; i < docs.length; i++) {
|
for (let i = 0; i < docs.length; i++) {
|
||||||
const task = docs[i];
|
const task = docs[i];
|
||||||
const _schedule = task.schedule && task.schedule.split(' ');
|
const _schedule = task.schedule && task.schedule.split(/ +/);
|
||||||
if (
|
if (
|
||||||
_schedule &&
|
_schedule &&
|
||||||
_schedule.length > 5 &&
|
_schedule.length > 5 &&
|
||||||
@@ -42,12 +30,19 @@ const run = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Logger.error(err);
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
app
|
app
|
||||||
.listen(config.cronPort, () => {
|
.listen(config.cronPort, async () => {
|
||||||
run();
|
await require('./loaders/sentry').default({ expressApp: app });
|
||||||
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
|
await run();
|
||||||
Logger.info(`
|
Logger.info(`
|
||||||
################################################
|
################################################
|
||||||
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
||||||
|
|||||||
@@ -1,333 +0,0 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import { getFileContentByName } from '../config/util';
|
|
||||||
import config from '../config';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import got from 'got';
|
|
||||||
import DataStore from 'nedb';
|
|
||||||
import { Cookie, CookieStatus, initCookiePosition } from '../data/cookie';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class CookieService {
|
|
||||||
private cronDb = new DataStore({ filename: config.cookieDbFile });
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
|
||||||
this.cronDb.loadDatabase((err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getCookies() {
|
|
||||||
const content = getFileContentByName(config.cookieFile);
|
|
||||||
return this.formatCookie(content.split('\n').filter((x) => !!x));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async addCookie(cookies: string[]) {
|
|
||||||
let content = getFileContentByName(config.cookieFile);
|
|
||||||
const originCookies = content.split('\n').filter((x) => !!x);
|
|
||||||
const result = originCookies.concat(cookies);
|
|
||||||
fs.writeFileSync(config.cookieFile, result.join('\n'));
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateCookie({ cookie, oldCookie }) {
|
|
||||||
let content = getFileContentByName(config.cookieFile);
|
|
||||||
const cookies = content.split('\n');
|
|
||||||
const index = cookies.findIndex((x) => x === oldCookie);
|
|
||||||
if (index !== -1) {
|
|
||||||
cookies[index] = cookie;
|
|
||||||
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
|
|
||||||
return '';
|
|
||||||
} else {
|
|
||||||
return '未找到要原有Cookie';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteCookie(cookie: string) {
|
|
||||||
let content = getFileContentByName(config.cookieFile);
|
|
||||||
const cookies = content.split('\n');
|
|
||||||
const index = cookies.findIndex((x) => x === cookie);
|
|
||||||
if (index !== -1) {
|
|
||||||
cookies.splice(index, 1);
|
|
||||||
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
|
|
||||||
return '';
|
|
||||||
} else {
|
|
||||||
return '未找到要删除的Cookie';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async formatCookie(data: any[]) {
|
|
||||||
const result = [];
|
|
||||||
for (const x of data) {
|
|
||||||
const { nickname, status } = await this.getJdInfo(x);
|
|
||||||
if (/pt_pin=(.+?);/.test(x)) {
|
|
||||||
result.push({
|
|
||||||
pin: x.match(/pt_pin=(.+?);/)[1],
|
|
||||||
cookie: x,
|
|
||||||
status,
|
|
||||||
nickname: nickname,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result.push({
|
|
||||||
pin: 'pin未匹配到',
|
|
||||||
cookie: x,
|
|
||||||
status,
|
|
||||||
nickname: nickname,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async refreshCookie(_id: string) {
|
|
||||||
const current = await this.get(_id);
|
|
||||||
const { status, nickname } = await this.getJdInfo(current.value);
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
status,
|
|
||||||
nickname,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getJdInfo(cookie: string) {
|
|
||||||
return fetch(
|
|
||||||
`https://me-api.jd.com/user_new/info/GetJDUserInfoUnion?orgFlag=JD_PinGou_New&callSource=mainorder&channel=4&isHomewhite=0&sceneval=2&_=${Date.now()}&sceneval=2&g_login_type=1&g_ty=ls`,
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
headers: {
|
|
||||||
Accept: '*/*',
|
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
|
||||||
'Accept-Language': 'zh-cn',
|
|
||||||
Connection: 'keep-alive',
|
|
||||||
Cookie: cookie,
|
|
||||||
Referer: 'https://home.m.jd.com/myJd/newhome.action',
|
|
||||||
'User-Agent':
|
|
||||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1',
|
|
||||||
Host: 'me-api.jd.com',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then((x) => x.json())
|
|
||||||
.then((x) => {
|
|
||||||
if (x.retcode === '0' && x.data && x.data.userInfo) {
|
|
||||||
return {
|
|
||||||
nickname: x.data.userInfo.baseInfo.nickname,
|
|
||||||
status: CookieStatus.normal,
|
|
||||||
};
|
|
||||||
} else if (x.retcode === 13) {
|
|
||||||
return { status: CookieStatus.invalid, nickname: '-' };
|
|
||||||
}
|
|
||||||
return { status: CookieStatus.abnormal, nickname: '-' };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async formatCookies(cookies: Cookie[]) {
|
|
||||||
const result = [];
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
const cookie = cookies[i];
|
|
||||||
if (cookie.status !== CookieStatus.disabled) {
|
|
||||||
const { status, nickname } = await this.getJdInfo(cookie.value);
|
|
||||||
result.push({ ...cookie, status, nickname });
|
|
||||||
} else {
|
|
||||||
result.push({ ...cookie, nickname: '-' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async create(payload: string[]): Promise<Cookie[]> {
|
|
||||||
const cookies = await this.cookies();
|
|
||||||
let position = initCookiePosition;
|
|
||||||
if (cookies && cookies.length > 0) {
|
|
||||||
position = cookies[cookies.length - 1].position;
|
|
||||||
}
|
|
||||||
const tabs = payload.map((x) => {
|
|
||||||
const cookie = new Cookie({ value: x, position });
|
|
||||||
position = position / 2;
|
|
||||||
cookie.position = position;
|
|
||||||
return cookie;
|
|
||||||
});
|
|
||||||
const docs = await this.insert(tabs);
|
|
||||||
await this.set_cookies();
|
|
||||||
return await this.formatCookies(docs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async insert(payload: Cookie[]): Promise<Cookie[]> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.cronDb.insert(payload, (err, docs) => {
|
|
||||||
if (err) {
|
|
||||||
this.logger.error(err);
|
|
||||||
} else {
|
|
||||||
resolve(docs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async update(payload: Cookie): Promise<Cookie> {
|
|
||||||
const { _id, ...other } = payload;
|
|
||||||
const doc = await this.get(_id);
|
|
||||||
const tab = new Cookie({ ...doc, ...other });
|
|
||||||
const newDoc = await this.updateDb(tab);
|
|
||||||
await this.set_cookies();
|
|
||||||
const [newCookie] = await this.formatCookies([newDoc]);
|
|
||||||
return newCookie;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateDb(payload: Cookie): Promise<Cookie> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.cronDb.update(
|
|
||||||
{ _id: payload._id },
|
|
||||||
payload,
|
|
||||||
{ returnUpdatedDocs: true },
|
|
||||||
(err, num, doc) => {
|
|
||||||
if (err) {
|
|
||||||
this.logger.error(err);
|
|
||||||
} else {
|
|
||||||
resolve(doc as Cookie);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove(ids: string[]) {
|
|
||||||
return new Promise((resolve: any) => {
|
|
||||||
this.cronDb.remove(
|
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ multi: true },
|
|
||||||
async (err) => {
|
|
||||||
await this.set_cookies();
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async move(
|
|
||||||
_id: string,
|
|
||||||
{
|
|
||||||
fromIndex,
|
|
||||||
toIndex,
|
|
||||||
}: {
|
|
||||||
fromIndex: number;
|
|
||||||
toIndex: number;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
let targetPosition: number;
|
|
||||||
const isUpward = fromIndex > toIndex;
|
|
||||||
const cookies = await this.cookies();
|
|
||||||
if (toIndex === 0 || toIndex === cookies.length - 1) {
|
|
||||||
targetPosition = isUpward
|
|
||||||
? cookies[0].position * 2
|
|
||||||
: cookies[toIndex].position / 2;
|
|
||||||
} else {
|
|
||||||
targetPosition = isUpward
|
|
||||||
? (cookies[toIndex].position + cookies[toIndex - 1].position) / 2
|
|
||||||
: (cookies[toIndex].position + cookies[toIndex + 1].position) / 2;
|
|
||||||
}
|
|
||||||
this.update({
|
|
||||||
_id,
|
|
||||||
position: targetPosition,
|
|
||||||
});
|
|
||||||
await this.set_cookies();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async cookies(
|
|
||||||
searchText?: string,
|
|
||||||
sort: any = { position: -1 },
|
|
||||||
needDetail: boolean = false,
|
|
||||||
): Promise<Cookie[]> {
|
|
||||||
let query = {};
|
|
||||||
if (searchText) {
|
|
||||||
const reg = new RegExp(searchText);
|
|
||||||
query = {
|
|
||||||
$or: [
|
|
||||||
{
|
|
||||||
name: reg,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: reg,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const newDocs = await this.find(query, sort);
|
|
||||||
if (needDetail) {
|
|
||||||
return await this.formatCookies(newDocs);
|
|
||||||
} else {
|
|
||||||
return newDocs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async find(query: any, sort: any): Promise<Cookie[]> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.cronDb
|
|
||||||
.find(query)
|
|
||||||
.sort({ ...sort })
|
|
||||||
.exec((err, docs) => {
|
|
||||||
resolve(docs);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get(_id: string): Promise<Cookie> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.cronDb.find({ _id }).exec((err, docs) => {
|
|
||||||
resolve(docs[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getBySort(sort: any): Promise<Cookie> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.cronDb
|
|
||||||
.find({})
|
|
||||||
.sort({ ...sort })
|
|
||||||
.limit(1)
|
|
||||||
.exec((err, docs) => {
|
|
||||||
resolve(docs[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async disabled(ids: string[]) {
|
|
||||||
return new Promise((resolve: any) => {
|
|
||||||
this.cronDb.update(
|
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ $set: { status: CookieStatus.disabled } },
|
|
||||||
{ multi: true },
|
|
||||||
async (err) => {
|
|
||||||
await this.set_cookies();
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async enabled(ids: string[]) {
|
|
||||||
return new Promise((resolve: any) => {
|
|
||||||
this.cronDb.update(
|
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ $set: { status: CookieStatus.noacquired } },
|
|
||||||
{ multi: true },
|
|
||||||
async (err, num) => {
|
|
||||||
await this.set_cookies();
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async set_cookies() {
|
|
||||||
const cookies = await this.cookies();
|
|
||||||
let cookie_string = '';
|
|
||||||
cookies.forEach((tab) => {
|
|
||||||
if (tab.status !== CookieStatus.disabled) {
|
|
||||||
cookie_string += tab.value;
|
|
||||||
cookie_string += '\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fs.writeFileSync(config.cookieFile, cookie_string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+360
-212
@@ -1,35 +1,24 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import DataStore from 'nedb';
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { Crontab, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
import { exec, execSync, spawn } from 'child_process';
|
import { exec, execSync, spawn } from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import cron_parser from 'cron-parser';
|
import cron_parser from 'cron-parser';
|
||||||
import { getFileContentByName } from '../config/util';
|
import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
|
||||||
import PQueue from 'p-queue';
|
import { promises, existsSync } from 'fs';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
import path from 'path';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
private cronDb = new DataStore({ filename: config.cronDbFile });
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
private queue = new PQueue({
|
|
||||||
concurrency: parseInt(process.env.MaxConcurrentNum) || 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
|
||||||
this.cronDb.loadDatabase((err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDb(): DataStore {
|
|
||||||
return this.cronDb;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -37,7 +26,6 @@ export default class CronService {
|
|||||||
|
|
||||||
public async create(payload: Crontab): Promise<Crontab> {
|
public async create(payload: Crontab): Promise<Crontab> {
|
||||||
const tab = new Crontab(payload);
|
const tab = new Crontab(payload);
|
||||||
tab.created = new Date().valueOf();
|
|
||||||
tab.saved = false;
|
tab.saved = false;
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
await this.set_crontab(this.isSixCron(doc));
|
await this.set_crontab(this.isSixCron(doc));
|
||||||
@@ -45,153 +33,248 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payload: Crontab): Promise<Crontab> {
|
public async insert(payload: Crontab): Promise<Crontab> {
|
||||||
return new Promise((resolve) => {
|
return await CrontabModel.create(payload, { returning: true });
|
||||||
this.cronDb.insert(payload, (err, docs) => {
|
|
||||||
if (err) {
|
|
||||||
this.logger.error(err);
|
|
||||||
} else {
|
|
||||||
resolve(docs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Crontab): Promise<Crontab> {
|
public async update(payload: Crontab): Promise<Crontab> {
|
||||||
const { _id, ...other } = payload;
|
payload.saved = false;
|
||||||
const doc = await this.get(_id);
|
const newDoc = await this.updateDb(payload);
|
||||||
const tab = new Crontab({ ...doc, ...other });
|
|
||||||
tab.saved = false;
|
|
||||||
const newDoc = await this.updateDb(tab);
|
|
||||||
await this.set_crontab(this.isSixCron(newDoc));
|
await this.set_crontab(this.isSixCron(newDoc));
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateDb(payload: Crontab): Promise<Crontab> {
|
public async updateDb(payload: Crontab): Promise<Crontab> {
|
||||||
return new Promise((resolve) => {
|
await CrontabModel.update(payload, { where: { id: payload.id } });
|
||||||
this.cronDb.update(
|
return await this.getDb({ id: payload.id });
|
||||||
{ _id: payload._id },
|
|
||||||
payload,
|
|
||||||
{ returnUpdatedDocs: true },
|
|
||||||
(err, num, docs: any) => {
|
|
||||||
if (err) {
|
|
||||||
this.logger.error(err);
|
|
||||||
} else {
|
|
||||||
resolve(docs);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async status({
|
public async status({
|
||||||
ids,
|
ids,
|
||||||
status,
|
status,
|
||||||
|
pid,
|
||||||
|
log_path,
|
||||||
|
last_running_time = 0,
|
||||||
|
last_execution_time = 0,
|
||||||
}: {
|
}: {
|
||||||
ids: string[];
|
ids: number[];
|
||||||
status: CrontabStatus;
|
status: CrontabStatus;
|
||||||
|
pid: number;
|
||||||
|
log_path: string;
|
||||||
|
last_running_time: number;
|
||||||
|
last_execution_time: number;
|
||||||
}) {
|
}) {
|
||||||
this.cronDb.update({ _id: { $in: ids } }, { $set: { status } });
|
const options: any = {
|
||||||
|
status,
|
||||||
|
pid,
|
||||||
|
log_path,
|
||||||
|
last_execution_time,
|
||||||
|
};
|
||||||
|
if (last_running_time > 0) {
|
||||||
|
options.last_running_time = last_running_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await CrontabModel.update({ ...options }, { where: { id: ids } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: string[]) {
|
public async remove(ids: number[]) {
|
||||||
return new Promise((resolve: any) => {
|
await CrontabModel.destroy({ where: { id: ids } });
|
||||||
this.cronDb.remove(
|
await this.set_crontab(true);
|
||||||
{ _id: { $in: ids } },
|
}
|
||||||
{ multi: true },
|
|
||||||
async (err) => {
|
public async pin(ids: number[]) {
|
||||||
await this.set_crontab(true);
|
await CrontabModel.update({ isPinned: 1 }, { where: { id: ids } });
|
||||||
resolve();
|
}
|
||||||
|
|
||||||
|
public async unPin(ids: number[]) {
|
||||||
|
await CrontabModel.update({ isPinned: 0 }, { where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addLabels(ids: string[], labels: string[]) {
|
||||||
|
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{
|
||||||
|
labels: Array.from(new Set((doc.labels || []).concat(labels))),
|
||||||
},
|
},
|
||||||
|
{ where: { id: doc.id } },
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeLabels(ids: string[], labels: string[]) {
|
||||||
|
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{
|
||||||
|
labels: (doc.labels || []).filter((label) => !labels.includes(label)),
|
||||||
|
},
|
||||||
|
{ where: { id: doc.id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
||||||
let query = {};
|
let query = {};
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const reg = new RegExp(searchText, 'i');
|
const textArray = searchText.split(':');
|
||||||
query = {
|
switch (textArray[0]) {
|
||||||
$or: [
|
case 'name':
|
||||||
{
|
case 'command':
|
||||||
name: reg,
|
case 'schedule':
|
||||||
},
|
case 'label':
|
||||||
{
|
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
|
||||||
command: reg,
|
query = {
|
||||||
},
|
[column]: {
|
||||||
{
|
[Op.or]: [
|
||||||
schedule: reg,
|
{ [Op.like]: `%${textArray[1]}%` },
|
||||||
},
|
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
||||||
],
|
],
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
const reg = {
|
||||||
|
[Op.or]: [
|
||||||
|
{ [Op.like]: `%${searchText}%` },
|
||||||
|
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
query = {
|
||||||
|
[Op.or]: [
|
||||||
|
{
|
||||||
|
name: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schedule: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labels: reg,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
this.cronDb
|
const result = await CrontabModel.findAll({
|
||||||
.find(query)
|
where: query,
|
||||||
.sort({ created: -1 })
|
order: [['createdAt', 'DESC']],
|
||||||
.exec((err, docs) => {
|
|
||||||
resolve(docs);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get(_id: string): Promise<Crontab> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.cronDb.find({ _id }).exec((err, docs) => {
|
|
||||||
resolve(docs[0]);
|
|
||||||
});
|
});
|
||||||
});
|
return result as any;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run(ids: string[]) {
|
public async getDb(query: any): Promise<Crontab> {
|
||||||
this.cronDb.update(
|
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
||||||
{ _id: { $in: ids } },
|
return doc && (doc.get({ plain: true }) as Crontab);
|
||||||
{ $set: { status: CrontabStatus.queued } },
|
}
|
||||||
{ multi: true },
|
|
||||||
|
public async run(ids: number[]) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.queued },
|
||||||
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
for (let i = 0; i < ids.length; i++) {
|
concurrentRun(
|
||||||
const id = ids[i];
|
ids.map((id) => async () => await this.runSingle(id)),
|
||||||
this.queue.add(() => this.runSingle(id));
|
10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(ids: number[]) {
|
||||||
|
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc.pid) {
|
||||||
|
try {
|
||||||
|
process.kill(-doc.pid);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.silly(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const err = await this.killTask(doc.command);
|
||||||
|
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||||
|
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||||
|
|
||||||
|
const endTime = dayjs();
|
||||||
|
const diffTimeStr = doc.last_execution_time
|
||||||
|
? `,耗时 ${endTime.diff(
|
||||||
|
dayjs(doc.last_execution_time * 1000),
|
||||||
|
'second',
|
||||||
|
)}`
|
||||||
|
: '';
|
||||||
|
if (logFileExist) {
|
||||||
|
const str = err ? `\n${err}` : '';
|
||||||
|
fs.appendFileSync(
|
||||||
|
`${absolutePath}`,
|
||||||
|
`${str}\n## 执行结束... ${endTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)}${diffTimeStr}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.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中不存在,所以截取前三个
|
||||||
|
pids = pids.slice(0, 3);
|
||||||
|
for (const id of pids) {
|
||||||
|
const c = `kill -9 ${id.slice(1)}`;
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execAsync(c);
|
||||||
|
if (stderr) {
|
||||||
|
killLogs.push(stderr);
|
||||||
|
}
|
||||||
|
if (stdout) {
|
||||||
|
killLogs.push(stdout);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
killLogs.push(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
||||||
|
} catch (e) {
|
||||||
|
return JSON.stringify(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(ids: string[]) {
|
private async runSingle(cronId: number): Promise<number> {
|
||||||
return new Promise((resolve: any) => {
|
|
||||||
this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => {
|
|
||||||
this.cronDb.update(
|
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
|
||||||
);
|
|
||||||
const pids = docs
|
|
||||||
.map((x) => x.pid)
|
|
||||||
.filter((x) => !!x)
|
|
||||||
.join('\n');
|
|
||||||
exec(`echo - e "${pids}" | xargs kill - 9`, (err) => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runSingle(id: string): Promise<number> {
|
|
||||||
return new Promise(async (resolve: any) => {
|
return new Promise(async (resolve: any) => {
|
||||||
const cron = await this.get(id);
|
const cron = await this.getDb({ id: cronId });
|
||||||
if (cron.status !== CrontabStatus.queued) {
|
if (cron.status !== CrontabStatus.queued) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { _id, command } = 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);
|
||||||
this.logger.silly('Original command: ' + command);
|
this.logger.silly('Original command: ' + command);
|
||||||
|
|
||||||
let logFile = `${config.manualLogPath}${_id}.log`;
|
|
||||||
fs.writeFileSync(
|
|
||||||
logFile,
|
|
||||||
`开始执行... ${new Date().toLocaleString()}\n\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
let cmdStr = command;
|
let cmdStr = command;
|
||||||
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
||||||
cmdStr = `task ${cmdStr}`;
|
cmdStr = `task ${cmdStr}`;
|
||||||
@@ -199,88 +282,159 @@ export default class CronService {
|
|||||||
if (cmdStr.endsWith('.js')) {
|
if (cmdStr.endsWith('.js')) {
|
||||||
cmdStr = `${cmdStr} now`;
|
cmdStr = `${cmdStr} now`;
|
||||||
}
|
}
|
||||||
const cmd = spawn(cmdStr, { shell: true });
|
|
||||||
|
|
||||||
this.cronDb.update(
|
const cp = spawn(cmdStr, { shell: '/bin/bash' });
|
||||||
{ _id },
|
|
||||||
{ $set: { status: CrontabStatus.running, pid: cmd.pid } },
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.running, pid: cp.pid },
|
||||||
|
{ where: { id } },
|
||||||
);
|
);
|
||||||
|
cp.stderr.on('data', (data) => {
|
||||||
cmd.stdout.on('data', (data) => {
|
if (logFileExist) {
|
||||||
this.logger.silly(`stdout: ${data}`);
|
fs.appendFileSync(`${absolutePath}`, `${data}`);
|
||||||
fs.appendFileSync(logFile, data);
|
}
|
||||||
|
});
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
if (logFileExist) {
|
||||||
|
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cmd.stderr.on('data', (data) => {
|
cp.on('exit', async (code, signal) => {
|
||||||
this.logger.silly(`stderr: ${data}`);
|
this.logger.info(
|
||||||
fs.appendFileSync(logFile, data);
|
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||||
});
|
|
||||||
|
|
||||||
cmd.on('close', (code) => {
|
|
||||||
this.logger.silly(`child process exited with code ${code}`);
|
|
||||||
this.cronDb.update(
|
|
||||||
{ _id },
|
|
||||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
|
||||||
);
|
);
|
||||||
});
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
cmd.on('error', (err) => {
|
{ where: { id } },
|
||||||
this.logger.info(err);
|
|
||||||
fs.appendFileSync(logFile, err.stack);
|
|
||||||
});
|
|
||||||
|
|
||||||
cmd.on('exit', (code: number, signal: any) => {
|
|
||||||
this.logger.silly(`cmd exit ${code}`);
|
|
||||||
this.cronDb.update(
|
|
||||||
{ _id },
|
|
||||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
|
||||||
);
|
);
|
||||||
fs.appendFileSync(logFile, `\n执行结束...`);
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
cp.on('close', async (code) => {
|
||||||
process.on('SIGINT', function () {
|
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||||
fs.appendFileSync(logFile, `\n执行结束...`);
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
|
{ where: { id } },
|
||||||
|
);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: string[]) {
|
public async disabled(ids: number[]) {
|
||||||
return new Promise((resolve: any) => {
|
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||||
this.cronDb.update(
|
await this.set_crontab(true);
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ $set: { isDisabled: 1 } },
|
|
||||||
{ multi: true },
|
|
||||||
async (err) => {
|
|
||||||
await this.set_crontab(true);
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: string[]) {
|
public async enabled(ids: number[]) {
|
||||||
return new Promise((resolve: any) => {
|
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||||
this.cronDb.update(
|
await this.set_crontab(true);
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ $set: { isDisabled: 0 } },
|
|
||||||
{ multi: true },
|
|
||||||
async (err) => {
|
|
||||||
await this.set_crontab(true);
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(_id: string) {
|
public async log(id: number) {
|
||||||
let logFile = `${config.manualLogPath}${_id}.log`;
|
const doc = await this.getDb({ id });
|
||||||
return getFileContentByName(logFile);
|
if (!doc) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||||
|
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||||
|
if (logFileExist) {
|
||||||
|
return getFileContentByName(`${absolutePath}`);
|
||||||
|
}
|
||||||
|
const [, commandStr, url] = doc.command.split(/ +/);
|
||||||
|
let logPath = this.getKey(commandStr);
|
||||||
|
const isQlCommand = doc.command.startsWith('ql ');
|
||||||
|
const key =
|
||||||
|
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
|
||||||
|
logPath;
|
||||||
|
if (isQlCommand) {
|
||||||
|
logPath = 'update';
|
||||||
|
}
|
||||||
|
let logDir = `${config.logPath}${logPath}`;
|
||||||
|
if (existsSync(logDir)) {
|
||||||
|
let files = await promises.readdir(logDir);
|
||||||
|
if (isQlCommand) {
|
||||||
|
files = files.filter((x) => x.includes(key));
|
||||||
|
}
|
||||||
|
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =
|
||||||
|
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
||||||
|
const end =
|
||||||
|
command.lastIndexOf('.') !== -1
|
||||||
|
? command.lastIndexOf('.')
|
||||||
|
: command.length;
|
||||||
|
|
||||||
|
const tmpStr = command.substring(0, start - 1);
|
||||||
|
let index = 0;
|
||||||
|
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
|
||||||
|
index = tmpStr.lastIndexOf('/');
|
||||||
|
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
|
||||||
|
index = tmpStr.lastIndexOf(':');
|
||||||
|
}
|
||||||
|
if (index) {
|
||||||
|
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
|
||||||
|
} else {
|
||||||
|
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}`;
|
||||||
return crontab_job_string;
|
return crontab_job_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,8 +442,8 @@ export default class CronService {
|
|||||||
const tabs = await this.crontabs();
|
const tabs = await this.crontabs();
|
||||||
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 += ' ';
|
||||||
@@ -310,44 +464,38 @@ export default class CronService {
|
|||||||
if (needReloadSchedule) {
|
if (needReloadSchedule) {
|
||||||
exec(`pm2 reload schedule`);
|
exec(`pm2 reload schedule`);
|
||||||
}
|
}
|
||||||
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
|
await CrontabModel.update({ saved: true }, { where: {} });
|
||||||
}
|
|
||||||
|
|
||||||
private reload_db() {
|
|
||||||
this.cronDb.loadDatabase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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((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;
|
||||||
|
|
||||||
this.cronDb.findOne({ command, schedule }, (err, doc) => {
|
const _crontab = await CrontabModel.findOne({
|
||||||
if (err) {
|
where: { command, schedule },
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
if (!doc) {
|
|
||||||
this.create({ name, command, schedule });
|
|
||||||
} else {
|
|
||||||
doc.command = command;
|
|
||||||
doc.schedule = schedule;
|
|
||||||
this.update(doc);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
if (!_crontab) {
|
||||||
|
await this.create({ name, command, schedule });
|
||||||
|
} else {
|
||||||
|
_crontab.command = command;
|
||||||
|
_crontab.schedule = schedule;
|
||||||
|
await this.update(_crontab);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import config from '../config';
|
||||||
|
import {
|
||||||
|
Dependence,
|
||||||
|
InstallDependenceCommandTypes,
|
||||||
|
DependenceStatus,
|
||||||
|
DependenceTypes,
|
||||||
|
unInstallDependenceCommandTypes,
|
||||||
|
DependenceModel,
|
||||||
|
} from '../data/dependence';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import SockService from './sock';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
import { concurrentRun } from '../config/util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class DependenceService {
|
||||||
|
constructor(
|
||||||
|
@Inject('logger') private logger: winston.Logger,
|
||||||
|
private sockService: SockService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
|
const tabs = payloads.map((x) => {
|
||||||
|
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
|
||||||
|
return tab;
|
||||||
|
});
|
||||||
|
const docs = await this.insert(tabs);
|
||||||
|
this.installDependenceOneByOne(docs);
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
|
const docs = await DependenceModel.bulkCreate(payloads);
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(
|
||||||
|
payload: Dependence & { id: string },
|
||||||
|
): Promise<Dependence> {
|
||||||
|
const { id, ...other } = payload;
|
||||||
|
const doc = await this.getDb({ id });
|
||||||
|
const tab = new Dependence({
|
||||||
|
...doc,
|
||||||
|
...other,
|
||||||
|
status: DependenceStatus.installing,
|
||||||
|
});
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
this.installDependenceOneByOne([newDoc]);
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateDb(payload: Dependence): Promise<Dependence> {
|
||||||
|
await DependenceModel.update(payload, { where: { id: payload.id } });
|
||||||
|
return await this.getDb({ id: payload.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.removing, log: [] },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
|
this.installDependenceOneByOne(docs, false, force);
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeDb(ids: number[]) {
|
||||||
|
await DependenceModel.destroy({ where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async dependencies(
|
||||||
|
{ searchValue, type }: { searchValue: string; type: string },
|
||||||
|
sort: any = { position: -1 },
|
||||||
|
query: any = {},
|
||||||
|
): Promise<Dependence[]> {
|
||||||
|
let condition = { ...query, type: DependenceTypes[type as any] };
|
||||||
|
if (searchValue) {
|
||||||
|
const encodeText = encodeURIComponent(searchValue);
|
||||||
|
const reg = {
|
||||||
|
[Op.or]: [
|
||||||
|
{ [Op.like]: `%${searchValue}%` },
|
||||||
|
{ [Op.like]: `%${encodeText}%` },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
condition = {
|
||||||
|
...condition,
|
||||||
|
name: reg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await this.find(condition);
|
||||||
|
return result as any;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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[]> {
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.installing, log: [] },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
|
||||||
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
|
this.installDependenceOneByOne(docs);
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async find(query: any, sort?: any): Promise<Dependence[]> {
|
||||||
|
const docs = await DependenceModel.findAll({ where: { ...query } });
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(query: any): Promise<Dependence> {
|
||||||
|
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
||||||
|
return doc && (doc.get({ plain: true }) as Dependence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateLog(ids: number[], log: string): Promise<void> {
|
||||||
|
const doc = await DependenceModel.findOne({ where: { id: ids } });
|
||||||
|
const newLog = doc?.log ? [...doc.log, log] : [log];
|
||||||
|
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public installOrUninstallDependencies(
|
||||||
|
dependencies: Dependence[],
|
||||||
|
isInstall: boolean = true,
|
||||||
|
force: boolean = false,
|
||||||
|
) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
if (dependencies.length === 0) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const socketMessageType = !force
|
||||||
|
? 'installDependence'
|
||||||
|
: 'uninstallDependence';
|
||||||
|
const depNames = dependencies.map((x) => x.name).join(' ');
|
||||||
|
const depRunCommand = (
|
||||||
|
isInstall
|
||||||
|
? InstallDependenceCommandTypes
|
||||||
|
: unInstallDependenceCommandTypes
|
||||||
|
)[dependencies[0].type as any];
|
||||||
|
const actionText = isInstall ? '安装' : '删除';
|
||||||
|
const depIds = dependencies.map((x) => x.id) as number[];
|
||||||
|
const startTime = dayjs();
|
||||||
|
|
||||||
|
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)}\n\n`;
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message,
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
await this.updateLog(depIds, message);
|
||||||
|
|
||||||
|
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
cp.stdout.on('data', async (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: data.toString(),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
await this.updateLog(depIds, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stderr.on('data', async (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: data.toString(),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
await this.updateLog(depIds, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('error', async (err) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: JSON.stringify(err),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
await this.updateLog(depIds, JSON.stringify(err));
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('close', async (code) => {
|
||||||
|
const endTime = dayjs();
|
||||||
|
const isSucceed = code === 0;
|
||||||
|
const resultText = isSucceed ? '成功' : '失败';
|
||||||
|
|
||||||
|
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message,
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
await this.updateLog(depIds, message);
|
||||||
|
|
||||||
|
let status = null;
|
||||||
|
if (isSucceed) {
|
||||||
|
status = isInstall
|
||||||
|
? DependenceStatus.installed
|
||||||
|
: DependenceStatus.removed;
|
||||||
|
} else {
|
||||||
|
status = isInstall
|
||||||
|
? DependenceStatus.installFailed
|
||||||
|
: DependenceStatus.removeFailed;
|
||||||
|
}
|
||||||
|
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||||
|
|
||||||
|
// 如果删除依赖成功或者强制删除
|
||||||
|
if ((isSucceed || force) && !isInstall) {
|
||||||
|
this.removeDb(depIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { getFileContentByName } from '../config/util';
|
||||||
|
import config from '../config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class EnvService {
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public async create(payloads: Env[]): Promise<Env[]> {
|
||||||
|
const envs = await this.envs();
|
||||||
|
let position = initEnvPosition;
|
||||||
|
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
|
||||||
|
position = envs[envs.length - 1].position as number;
|
||||||
|
}
|
||||||
|
const tabs = payloads.map((x) => {
|
||||||
|
position = position / 2;
|
||||||
|
const tab = new Env({ ...x, position });
|
||||||
|
return tab;
|
||||||
|
});
|
||||||
|
const docs = await this.insert(tabs);
|
||||||
|
await this.set_envs();
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insert(payloads: Env[]): Promise<Env[]> {
|
||||||
|
const result = [];
|
||||||
|
for (const env of payloads) {
|
||||||
|
const doc = await EnvModel.create(env, { returning: true });
|
||||||
|
result.push(doc);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(payload: Env): Promise<Env> {
|
||||||
|
const newDoc = await this.updateDb(payload);
|
||||||
|
await this.set_envs();
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateDb(payload: Env): Promise<Env> {
|
||||||
|
await EnvModel.update({ ...payload }, { where: { id: payload.id } });
|
||||||
|
return await this.getDb({ id: payload.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(ids: string[]) {
|
||||||
|
await EnvModel.destroy({ where: { id: ids } });
|
||||||
|
await this.set_envs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async move(
|
||||||
|
id: number,
|
||||||
|
{
|
||||||
|
fromIndex,
|
||||||
|
toIndex,
|
||||||
|
}: {
|
||||||
|
fromIndex: number;
|
||||||
|
toIndex: number;
|
||||||
|
},
|
||||||
|
): Promise<Env> {
|
||||||
|
let targetPosition: number;
|
||||||
|
const isUpward = fromIndex > toIndex;
|
||||||
|
const envs = await this.envs();
|
||||||
|
if (toIndex === 0 || toIndex === envs.length - 1) {
|
||||||
|
targetPosition = isUpward
|
||||||
|
? envs[0].position * 2
|
||||||
|
: envs[toIndex].position / 2;
|
||||||
|
} else {
|
||||||
|
targetPosition = isUpward
|
||||||
|
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
|
||||||
|
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
||||||
|
}
|
||||||
|
const newDoc = await this.update({
|
||||||
|
id,
|
||||||
|
position: targetPosition,
|
||||||
|
});
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async envs(
|
||||||
|
searchText: string = '',
|
||||||
|
sort: any = { position: -1 },
|
||||||
|
query: any = {},
|
||||||
|
): Promise<Env[]> {
|
||||||
|
let condition = { ...query };
|
||||||
|
if (searchText) {
|
||||||
|
const encodeText = encodeURIComponent(searchText);
|
||||||
|
const reg = {
|
||||||
|
[Op.or]: [
|
||||||
|
{ [Op.like]: `%${searchText}%` },
|
||||||
|
{ [Op.like]: `%${encodeText}%` },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
condition = {
|
||||||
|
...condition,
|
||||||
|
[Op.or]: [
|
||||||
|
{
|
||||||
|
name: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
remarks: reg,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await this.find(condition, [['position', 'DESC']]);
|
||||||
|
return result as any;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async find(query: any, sort: any = []): Promise<Env[]> {
|
||||||
|
const docs = await EnvModel.findAll({
|
||||||
|
where: { ...query },
|
||||||
|
order: [...sort],
|
||||||
|
});
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(query: any): Promise<Env> {
|
||||||
|
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
||||||
|
return doc && (doc.get({ plain: true }) as Env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disabled(ids: string[]) {
|
||||||
|
await EnvModel.update(
|
||||||
|
{ status: EnvStatus.disabled },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
await this.set_envs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enabled(ids: string[]) {
|
||||||
|
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
||||||
|
await this.set_envs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
||||||
|
await EnvModel.update({ name }, { where: { id: ids } });
|
||||||
|
await this.set_envs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async set_envs() {
|
||||||
|
const envs = await this.envs(
|
||||||
|
'',
|
||||||
|
{ position: -1 },
|
||||||
|
{ name: { [Op.not]: null } },
|
||||||
|
);
|
||||||
|
const groups = _.groupBy(envs, 'name');
|
||||||
|
let env_string = '';
|
||||||
|
for (const key in groups) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||||
|
const group = groups[key];
|
||||||
|
|
||||||
|
// 忽略不符合bash要求的环境变量名称
|
||||||
|
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
||||||
|
let value = _(group)
|
||||||
|
.filter((x) => x.status !== EnvStatus.disabled)
|
||||||
|
.map('value')
|
||||||
|
.join('&')
|
||||||
|
.replace(/(\\)[^n]/g, '\\\\')
|
||||||
|
.replace(/(\\$)/, '\\\\')
|
||||||
|
.replace(/"/g, '\\"')
|
||||||
|
.trim();
|
||||||
|
env_string += `export ${key}="${value}"\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(config.envFile, env_string);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import UserService from './user';
|
||||||
|
import got from 'got';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class NotificationService {
|
||||||
|
@Inject((type) => UserService)
|
||||||
|
private userService!: UserService;
|
||||||
|
|
||||||
|
private modeMap = new Map([
|
||||||
|
['gotify', this.gotify],
|
||||||
|
['goCqHttpBot', this.goCqHttpBot],
|
||||||
|
['serverChan', this.serverChan],
|
||||||
|
['pushDeer', this.pushDeer],
|
||||||
|
['bark', this.bark],
|
||||||
|
['telegramBot', this.telegramBot],
|
||||||
|
['dingtalkBot', this.dingtalkBot],
|
||||||
|
['weWorkBot', this.weWorkBot],
|
||||||
|
['weWorkApp', this.weWorkApp],
|
||||||
|
['iGot', this.iGot],
|
||||||
|
['pushPlus', this.pushPlus],
|
||||||
|
['email', this.email],
|
||||||
|
]);
|
||||||
|
|
||||||
|
private timeout = 10000;
|
||||||
|
private title = '';
|
||||||
|
private content = '';
|
||||||
|
private params!: Omit<NotificationInfo, 'type'>;
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public async notify(
|
||||||
|
title: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<boolean | undefined> {
|
||||||
|
const { type, ...rest } = await this.userService.getNotificationMode();
|
||||||
|
if (type) {
|
||||||
|
this.title = title;
|
||||||
|
this.content = content;
|
||||||
|
this.params = rest;
|
||||||
|
const notificationModeAction = this.modeMap.get(type);
|
||||||
|
try {
|
||||||
|
return await notificationModeAction?.call(this);
|
||||||
|
} catch (error: any) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testNotify(
|
||||||
|
info: NotificationInfo,
|
||||||
|
title: string,
|
||||||
|
content: string,
|
||||||
|
) {
|
||||||
|
const { type, ...rest } = info;
|
||||||
|
if (type) {
|
||||||
|
this.title = title;
|
||||||
|
this.content = content;
|
||||||
|
this.params = rest;
|
||||||
|
const notificationModeAction = this.modeMap.get(type);
|
||||||
|
return await notificationModeAction?.call(this);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async gotify() {
|
||||||
|
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
||||||
|
const res: any = await got
|
||||||
|
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
body: `title=${encodeURIComponent(
|
||||||
|
this.title,
|
||||||
|
)}&message=${encodeURIComponent(
|
||||||
|
this.content,
|
||||||
|
)}&priority=${gotifyPriority}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return typeof res.id === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async goCqHttpBot() {
|
||||||
|
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||||
|
const res: any = await got
|
||||||
|
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: { message: `${this.title}\n${this.content}` },
|
||||||
|
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.retcode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async serverChan() {
|
||||||
|
const { serverChanKey } = this.params;
|
||||||
|
const url = serverChanKey.startsWith('SCT')
|
||||||
|
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
||||||
|
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
body: `title=${this.title}&desp=${this.content}`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
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() {
|
||||||
|
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
||||||
|
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
||||||
|
barkPush = `https://api.day.app/${barkPush}`;
|
||||||
|
}
|
||||||
|
const url = `${barkPush}/${encodeURIComponent(
|
||||||
|
this.title,
|
||||||
|
)}/${encodeURIComponent(
|
||||||
|
this.content,
|
||||||
|
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
|
||||||
|
const res: any = await got
|
||||||
|
.get(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.code === 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async telegramBot() {
|
||||||
|
const {
|
||||||
|
telegramBotApiHost,
|
||||||
|
telegramBotProxyAuth,
|
||||||
|
telegramBotProxyHost,
|
||||||
|
telegramBotProxyPort,
|
||||||
|
telegramBotToken,
|
||||||
|
telegramBotUserId,
|
||||||
|
} = this.params;
|
||||||
|
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
|
||||||
|
const url = `https://${
|
||||||
|
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
|
||||||
|
}/bot${telegramBotToken}/sendMessage`;
|
||||||
|
let agent;
|
||||||
|
if (telegramBotProxyHost && telegramBotProxyPort) {
|
||||||
|
const options: any = {
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveMsecs: 1000,
|
||||||
|
maxSockets: 256,
|
||||||
|
maxFreeSockets: 256,
|
||||||
|
proxy: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
|
||||||
|
};
|
||||||
|
const httpAgent = new HttpProxyAgent(options);
|
||||||
|
const httpsAgent = new HttpsProxyAgent(options);
|
||||||
|
agent = {
|
||||||
|
http: httpAgent,
|
||||||
|
https: httpsAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
agent,
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return !!res.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async dingtalkBot() {
|
||||||
|
const { dingtalkBotSecret, dingtalkBotToken } = this.params;
|
||||||
|
let secretParam = '';
|
||||||
|
if (dingtalkBotSecret) {
|
||||||
|
const dateNow = Date.now();
|
||||||
|
const hmac = crypto.createHmac('sha256', dingtalkBotSecret);
|
||||||
|
hmac.update(`${dateNow}\n${dingtalkBotSecret}`);
|
||||||
|
const result = encodeURIComponent(hmac.digest('base64'));
|
||||||
|
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
||||||
|
}
|
||||||
|
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
msgtype: 'text',
|
||||||
|
text: {
|
||||||
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.errcode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async weWorkBot() {
|
||||||
|
const { weWorkBotKey } = this.params;
|
||||||
|
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
msgtype: 'text',
|
||||||
|
text: {
|
||||||
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.errcode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async weWorkApp() {
|
||||||
|
const { weWorkAppKey } = this.params;
|
||||||
|
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||||
|
weWorkAppKey.split(',');
|
||||||
|
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
||||||
|
const { access_token } = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
corpid,
|
||||||
|
corpsecret,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
let options: any = {
|
||||||
|
msgtype: 'mpnews',
|
||||||
|
mpnews: {
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
title: `${this.title}`,
|
||||||
|
thumb_media_id,
|
||||||
|
author: `智能助手`,
|
||||||
|
content_source_url: ``,
|
||||||
|
content: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||||
|
digest: `${this.content}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
switch (thumb_media_id) {
|
||||||
|
case '0':
|
||||||
|
options = {
|
||||||
|
msgtype: 'textcard',
|
||||||
|
textcard: {
|
||||||
|
title: `${this.title}`,
|
||||||
|
description: `${this.content}`,
|
||||||
|
url: 'https://github.com/whyour/qinglong',
|
||||||
|
btntxt: '更多',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '1':
|
||||||
|
options = {
|
||||||
|
msgtype: 'text',
|
||||||
|
text: {
|
||||||
|
content: `${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await got
|
||||||
|
.post(
|
||||||
|
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
|
||||||
|
{
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
touser,
|
||||||
|
agentid,
|
||||||
|
safe: '0',
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.errcode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async iGot() {
|
||||||
|
const { iGotPushKey } = this.params;
|
||||||
|
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
body: `title=${this.title}&content=${this.content}`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.ret === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pushPlus() {
|
||||||
|
const { pushPlusToken, pushPlusUser } = this.params;
|
||||||
|
const url = `https://www.pushplus.plus/send`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
token: `${pushPlusToken}`,
|
||||||
|
title: `${this.title}`,
|
||||||
|
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||||
|
topic: `${pushPlusUser || ''}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.code === 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async email() {
|
||||||
|
const { emailPass, emailService, emailUser } = this.params;
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
service: emailService,
|
||||||
|
auth: {
|
||||||
|
user: emailUser,
|
||||||
|
pass: emailPass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: `"青龙快讯" <${emailUser}>`,
|
||||||
|
to: `${emailUser}`,
|
||||||
|
subject: `${this.title}`,
|
||||||
|
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
transporter.close();
|
||||||
|
|
||||||
|
return !!info.messageId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { createRandomString } from '../config/util';
|
||||||
|
import config from '../config';
|
||||||
|
import { App, AppModel } from '../data/open';
|
||||||
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
import sequelize, { Op } from 'sequelize';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class OpenService {
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public async findTokenByValue(token: string): Promise<App | null> {
|
||||||
|
const docs = await this.find({});
|
||||||
|
const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token));
|
||||||
|
return doc[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(payload: App): Promise<App> {
|
||||||
|
const tab = { ...payload };
|
||||||
|
tab.client_id = createRandomString(12, 12);
|
||||||
|
tab.client_secret = createRandomString(24, 24);
|
||||||
|
const doc = await this.insert(tab);
|
||||||
|
return { ...doc, tokens: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insert(payload: App): Promise<App> {
|
||||||
|
const doc = await AppModel.create(payload, { returning: true });
|
||||||
|
return doc.get({ plain: true }) as App;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(payload: App): Promise<App> {
|
||||||
|
const newDoc = await this.updateDb({
|
||||||
|
name: payload.name,
|
||||||
|
scopes: payload.scopes,
|
||||||
|
id: payload.id,
|
||||||
|
} as any);
|
||||||
|
return { ...newDoc, tokens: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateDb(payload: App): Promise<App> {
|
||||||
|
await AppModel.update(payload, { where: { id: payload.id } });
|
||||||
|
return await this.getDb({ id: payload.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(query: any): Promise<App> {
|
||||||
|
const doc: any = await AppModel.findOne({ where: query });
|
||||||
|
return doc && (doc.get({ plain: true }) as App);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(ids: number[]) {
|
||||||
|
await AppModel.destroy({ where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resetSecret(id: number): Promise<App> {
|
||||||
|
const tab: any = {
|
||||||
|
client_secret: createRandomString(24, 24),
|
||||||
|
tokens: [],
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
// const doc = await this.get(id);
|
||||||
|
// const tab = new App({ ...doc });
|
||||||
|
// tab.client_secret = createRandomString(24, 24);
|
||||||
|
// tab.tokens = [];
|
||||||
|
// const newDoc = await this.updateDb(tab);
|
||||||
|
// return newDoc;
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(
|
||||||
|
searchText: string = '',
|
||||||
|
sort: any = {},
|
||||||
|
query: any = {},
|
||||||
|
): Promise<App[]> {
|
||||||
|
let condition = { ...query };
|
||||||
|
if (searchText) {
|
||||||
|
const encodeText = encodeURIComponent(searchText);
|
||||||
|
const reg = {
|
||||||
|
[Op.or]: [
|
||||||
|
{ [Op.like]: `%${searchText}%` },
|
||||||
|
{ [Op.like]: `%${encodeText}%` },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
condition = {
|
||||||
|
...condition,
|
||||||
|
name: reg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await this.find(condition);
|
||||||
|
return result
|
||||||
|
.filter((x) => x.name !== 'system')
|
||||||
|
.map((x) => ({ ...x, tokens: [] }));
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async find(query: any, sort?: any): Promise<App[]> {
|
||||||
|
const docs = await AppModel.findAll({ where: { ...query } });
|
||||||
|
return docs.map((x) => x.get({ plain: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async authToken({
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
}: {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
let token = uuidV4();
|
||||||
|
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
|
||||||
|
const doc = await AppModel.findOne({ where: { client_id, client_secret } });
|
||||||
|
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(
|
||||||
|
{ tokens },
|
||||||
|
{ where: { client_id, client_secret } },
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
token_type: 'Bearer',
|
||||||
|
expiration,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { code: 400, message: 'client_id或client_seret有误' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findSystemToken(): 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 nowTime = Math.round(Date.now() / 1000);
|
||||||
|
let token;
|
||||||
|
if (
|
||||||
|
!systemApp.tokens ||
|
||||||
|
!systemApp.tokens.length ||
|
||||||
|
nowTime > [...systemApp.tokens].pop()!.expiration
|
||||||
|
) {
|
||||||
|
const authToken = await this.authToken({
|
||||||
|
client_id: systemApp.client_id,
|
||||||
|
client_secret: systemApp.client_secret,
|
||||||
|
});
|
||||||
|
token = authToken.data;
|
||||||
|
token.value = token.token;
|
||||||
|
} else {
|
||||||
|
token = [...systemApp.tokens].pop();
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { ChildProcessWithoutNullStreams, exec, spawn } 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()
|
||||||
|
export default class ScheduleService {
|
||||||
|
private scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||||
|
|
||||||
|
private intervalSchedule = new ToadScheduler();
|
||||||
|
|
||||||
|
private maxBuffer = 200 * 1024 * 1024;
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
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(
|
||||||
|
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('close', async (code) => {
|
||||||
|
const endTime = dayjs();
|
||||||
|
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||||
|
await callbacks.onEnd?.(
|
||||||
|
cp,
|
||||||
|
endTime,
|
||||||
|
endTime.diff(startTime, 'seconds'),
|
||||||
|
);
|
||||||
|
resolve(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,
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.scheduleStacks.set(
|
||||||
|
_id,
|
||||||
|
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||||
|
await this.runTask(command, callbacks);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (runImmediately) {
|
||||||
|
await this.runTask(command, callbacks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
||||||
|
const _id = this.formatId(id);
|
||||||
|
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||||
|
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(
|
||||||
|
{ ...schedule, runImmediately: false },
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import SockService from './sock';
|
||||||
|
import CronService from './cron';
|
||||||
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class ScriptService {
|
||||||
|
constructor(
|
||||||
|
@Inject('logger') private logger: winston.Logger,
|
||||||
|
private sockService: SockService,
|
||||||
|
private cronService: CronService,
|
||||||
|
private scheduleService: ScheduleService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private taskCallbacks(filePath: string): TaskCallbacks {
|
||||||
|
return {
|
||||||
|
onEnd: async (cp, endTime, diff) => {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
onError: async (message: string) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'manuallyRunScript',
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onLog: async (message: string) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'manuallyRunScript',
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runScript(filePath: string) {
|
||||||
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
|
const command = `task -l ${relativePath} now`;
|
||||||
|
this.scheduleService.runTask(command, this.taskCallbacks(filePath));
|
||||||
|
|
||||||
|
return { code: 200 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stopScript(filePath: string) {
|
||||||
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
|
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
|
||||||
|
|
||||||
|
const str = err ? `\n${err}` : '';
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'manuallyRunScript',
|
||||||
|
message: `${str}\n## 执行结束... ${new Date()
|
||||||
|
.toLocaleString('zh', { hour12: false })
|
||||||
|
.replace(' 24:', ' 00:')} `,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { code: 200 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { Connection } from 'sockjs';
|
||||||
|
import { SockMessage } from '../data/sock';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class SockService {
|
||||||
|
private clients: Connection[] = [];
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public getClients() {
|
||||||
|
return this.clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addClient(conn: Connection) {
|
||||||
|
if (this.clients.indexOf(conn) === -1) {
|
||||||
|
this.clients.push(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeClient(conn: Connection) {
|
||||||
|
const index = this.clients.indexOf(conn);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.clients.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendMessage(msg: SockMessage) {
|
||||||
|
this.clients.forEach((x) => {
|
||||||
|
x.write(JSON.stringify(msg));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import config from '../config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import NotificationService from './notify';
|
||||||
|
import ScheduleService from './schedule';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import SockService from './sock';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class SystemService {
|
||||||
|
@Inject((type) => NotificationService)
|
||||||
|
private notificationService!: NotificationService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('logger') private logger: winston.Logger,
|
||||||
|
private scheduleService: ScheduleService,
|
||||||
|
private sockService: SockService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async getLogRemoveFrequency() {
|
||||||
|
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
|
||||||
|
return doc || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||||
|
await AuthModel.upsert({ ...payload });
|
||||||
|
const doc = await this.getDb({ type: payload.type });
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(query: any): Promise<any> {
|
||||||
|
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||||
|
return doc && (doc.get({ plain: true }) as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||||
|
const code = Math.random().toString().slice(-6);
|
||||||
|
const isSuccess = await this.notificationService.testNotify(
|
||||||
|
notificationInfo,
|
||||||
|
'青龙',
|
||||||
|
`【蛟龙】测试通知 https://t.me/jiao_long`,
|
||||||
|
);
|
||||||
|
if (isSuccess) {
|
||||||
|
const result = await this.updateAuthDb({
|
||||||
|
type: AuthDataType.notification,
|
||||||
|
info: { ...notificationInfo },
|
||||||
|
});
|
||||||
|
return { code: 200, data: { ...result, code } };
|
||||||
|
} else {
|
||||||
|
return { code: 400, message: '通知发送失败,请检查参数' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateLogRemoveFrequency(frequency: number) {
|
||||||
|
const oDoc = await this.getLogRemoveFrequency();
|
||||||
|
const result = await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
type: AuthDataType.removeLogFrequency,
|
||||||
|
info: { frequency },
|
||||||
|
});
|
||||||
|
const cron = {
|
||||||
|
id: result.id,
|
||||||
|
name: '删除日志',
|
||||||
|
command: `ql rmlog ${frequency}`,
|
||||||
|
};
|
||||||
|
await this.scheduleService.cancelIntervalTask(cron);
|
||||||
|
if (frequency > 0) {
|
||||||
|
await this.scheduleService.createIntervalTask(cron, {
|
||||||
|
days: frequency,
|
||||||
|
runImmediately: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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 got.get(config.lastVersionFile, {
|
||||||
|
timeout: 6000,
|
||||||
|
retry: 0,
|
||||||
|
});
|
||||||
|
const lastVersionFileContent = result.body;
|
||||||
|
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||||
|
lastLog = lastVersionFileContent.match(logRegx)
|
||||||
|
? lastVersionFileContent.match(logRegx)![1]
|
||||||
|
: '';
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
||||||
|
lastVersion,
|
||||||
|
lastLog,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
code: 400,
|
||||||
|
message: 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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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: '通知发送失败,请检查系统设置/通知配置' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
||||||
|
import config from '../config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { authenticator } from '@otplib/preset-default';
|
||||||
|
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import NotificationService from './notify';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import ScheduleService from './schedule';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import SockService from './sock';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class UserService {
|
||||||
|
@Inject((type) => NotificationService)
|
||||||
|
private notificationService!: NotificationService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('logger') private logger: winston.Logger,
|
||||||
|
private scheduleService: ScheduleService,
|
||||||
|
private sockService: SockService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async login(
|
||||||
|
payloads: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
},
|
||||||
|
req: Request,
|
||||||
|
needTwoFactor = true,
|
||||||
|
): Promise<any> {
|
||||||
|
if (!fs.existsSync(config.authConfigFile)) {
|
||||||
|
return this.initAuthInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
let { username, password } = payloads;
|
||||||
|
const content = this.getAuthInfo();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
if (content) {
|
||||||
|
let {
|
||||||
|
username: cUsername,
|
||||||
|
password: cPassword,
|
||||||
|
retries = 0,
|
||||||
|
lastlogon,
|
||||||
|
lastip,
|
||||||
|
lastaddr,
|
||||||
|
twoFactorActivated,
|
||||||
|
twoFactorActived,
|
||||||
|
tokens = {},
|
||||||
|
platform,
|
||||||
|
} = content;
|
||||||
|
// patch old field
|
||||||
|
twoFactorActivated = twoFactorActivated || twoFactorActived;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(cUsername === 'admin' && cPassword === 'admin') ||
|
||||||
|
!cUsername ||
|
||||||
|
!cPassword
|
||||||
|
) {
|
||||||
|
return this.initAuthInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
|
||||||
|
return {
|
||||||
|
code: 410,
|
||||||
|
message: `失败次数过多,请${Math.round(
|
||||||
|
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||||
|
)}秒后重试`,
|
||||||
|
data: Math.round(
|
||||||
|
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ip, address } = await getNetIp(req);
|
||||||
|
if (username === cUsername && password === cPassword) {
|
||||||
|
if (twoFactorActivated && needTwoFactor) {
|
||||||
|
this.updateAuthInfo(content, {
|
||||||
|
isTwoFactorChecking: true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
code: 420,
|
||||||
|
message: '请输入两步验证token',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = createRandomString(50, 100);
|
||||||
|
const expiration = twoFactorActivated ? 30 : 3;
|
||||||
|
let token = jwt.sign({ data }, config.secret as any, {
|
||||||
|
expiresIn: 60 * 60 * 24 * expiration,
|
||||||
|
algorithm: 'HS384',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateAuthInfo(content, {
|
||||||
|
token,
|
||||||
|
tokens: {
|
||||||
|
...tokens,
|
||||||
|
[req.platform]: token,
|
||||||
|
},
|
||||||
|
lastlogon: timestamp,
|
||||||
|
retries: 0,
|
||||||
|
lastip: ip,
|
||||||
|
lastaddr: address,
|
||||||
|
platform: req.platform,
|
||||||
|
isTwoFactorChecking: false,
|
||||||
|
});
|
||||||
|
await this.notificationService.notify(
|
||||||
|
'登录通知',
|
||||||
|
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||||
|
req.platform
|
||||||
|
}端 登录成功,ip地址 ${ip}`,
|
||||||
|
);
|
||||||
|
await this.getLoginLog();
|
||||||
|
await this.insertDb({
|
||||||
|
type: AuthDataType.loginLog,
|
||||||
|
info: {
|
||||||
|
timestamp,
|
||||||
|
address,
|
||||||
|
ip,
|
||||||
|
platform: req.platform,
|
||||||
|
status: LoginStatus.success,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.updateAuthInfo(content, {
|
||||||
|
retries: retries + 1,
|
||||||
|
lastlogon: timestamp,
|
||||||
|
lastip: ip,
|
||||||
|
lastaddr: address,
|
||||||
|
platform: req.platform,
|
||||||
|
});
|
||||||
|
await this.notificationService.notify(
|
||||||
|
'登录通知',
|
||||||
|
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||||
|
req.platform
|
||||||
|
}端 登录失败,ip地址 ${ip}`,
|
||||||
|
);
|
||||||
|
await this.getLoginLog();
|
||||||
|
await this.insertDb({
|
||||||
|
type: AuthDataType.loginLog,
|
||||||
|
info: {
|
||||||
|
timestamp,
|
||||||
|
address,
|
||||||
|
ip,
|
||||||
|
platform: req.platform,
|
||||||
|
status: LoginStatus.fail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { code: 400, message: config.authError };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.initAuthInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(platform: string): Promise<any> {
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
this.updateAuthInfo(authInfo, {
|
||||||
|
token: '',
|
||||||
|
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLoginLog(): Promise<AuthInfo[]> {
|
||||||
|
const docs = await AuthModel.findAll({
|
||||||
|
where: { type: AuthDataType.loginLog },
|
||||||
|
});
|
||||||
|
if (docs && docs.length > 0) {
|
||||||
|
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
|
||||||
|
if (result.length > 100) {
|
||||||
|
await AuthModel.destroy({
|
||||||
|
where: { id: result[result.length - 1].id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result.map((x) => x.info);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
|
||||||
|
const doc = await AuthModel.create({ ...payload }, { returning: true });
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initAuthInfo() {
|
||||||
|
const newPassword = createRandomString(16, 22);
|
||||||
|
fs.writeFileSync(
|
||||||
|
config.authConfigFile,
|
||||||
|
JSON.stringify({
|
||||||
|
username: 'admin',
|
||||||
|
password: newPassword,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
code: 100,
|
||||||
|
message: '已初始化密码,请前往auth.json查看并重新登录',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateUsernameAndPassword({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}) {
|
||||||
|
if (password === 'admin') {
|
||||||
|
return { code: 400, message: '密码不能设置为admin' };
|
||||||
|
}
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
this.updateAuthInfo(authInfo, { username, password });
|
||||||
|
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> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
||||||
|
if (err) console.log(err);
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public initTwoFactor() {
|
||||||
|
const secret = authenticator.generateSecret();
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
||||||
|
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||||
|
return { secret, url: otpauth };
|
||||||
|
}
|
||||||
|
|
||||||
|
public activeTwoFactor(code: string) {
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
const isValid = authenticator.verify({
|
||||||
|
token: code,
|
||||||
|
secret: authInfo.twoFactorSecret,
|
||||||
|
});
|
||||||
|
if (isValid) {
|
||||||
|
this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async twoFactorLogin(
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
code,
|
||||||
|
}: { username: string; password: string; code: string },
|
||||||
|
req: any,
|
||||||
|
) {
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
|
||||||
|
if (!isTwoFactorChecking) {
|
||||||
|
return { code: 450, message: '未知错误' };
|
||||||
|
}
|
||||||
|
const isValid = authenticator.verify({
|
||||||
|
token: code,
|
||||||
|
secret: twoFactorSecret,
|
||||||
|
});
|
||||||
|
if (isValid) {
|
||||||
|
return this.login({ username, password }, req, false);
|
||||||
|
} else {
|
||||||
|
const { ip, address } = await getNetIp(req);
|
||||||
|
this.updateAuthInfo(authInfo, {
|
||||||
|
lastip: ip,
|
||||||
|
lastaddr: address,
|
||||||
|
platform: req.platform,
|
||||||
|
});
|
||||||
|
return { code: 430, message: '验证失败' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public deactiveTwoFactor() {
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
this.updateAuthInfo(authInfo, {
|
||||||
|
twoFactorActivated: false,
|
||||||
|
twoFactorActived: false,
|
||||||
|
twoFactorSecret: '',
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAuthInfo() {
|
||||||
|
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
|
return JSON.parse(content || '{}');
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateAuthInfo(authInfo: any, info: any) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
config.authConfigFile,
|
||||||
|
JSON.stringify({ ...authInfo, ...info }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getNotificationMode(): Promise<NotificationInfo> {
|
||||||
|
const doc = await this.getDb({ type: AuthDataType.notification });
|
||||||
|
return (doc && doc.info) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||||
|
let doc = await AuthModel.findOne({ type: payload.type });
|
||||||
|
if (doc) {
|
||||||
|
const updateResult = await AuthModel.update(payload, {
|
||||||
|
where: { id: doc.id },
|
||||||
|
returning: true,
|
||||||
|
});
|
||||||
|
doc = updateResult[1][0];
|
||||||
|
} else {
|
||||||
|
doc = await AuthModel.create(payload, { returning: true });
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(query: any): Promise<any> {
|
||||||
|
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||||
|
return doc && (doc.get({ plain: true }) as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||||
|
const code = Math.random().toString().slice(-6);
|
||||||
|
const isSuccess = await this.notificationService.testNotify(
|
||||||
|
notificationInfo,
|
||||||
|
'青龙',
|
||||||
|
`【蛟龙】测试通知 https://t.me/jiao_long`,
|
||||||
|
);
|
||||||
|
if (isSuccess) {
|
||||||
|
const result = await this.updateAuthDb({
|
||||||
|
type: AuthDataType.notification,
|
||||||
|
info: { ...notificationInfo },
|
||||||
|
});
|
||||||
|
return { code: 200, data: { ...result, code } };
|
||||||
|
} else {
|
||||||
|
return { code: 400, message: '通知发送失败,请检查参数' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
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 {
|
||||||
|
const data = await readFile();
|
||||||
|
const nowTime = Math.round(Date.now() / 1000);
|
||||||
|
if (data.value && data.expiration > nowTime) {
|
||||||
|
console.log(data.value);
|
||||||
|
} else {
|
||||||
|
Container.set('logger', LoggerInstance);
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const appToken = await openService.findSystemToken();
|
||||||
|
console.log(appToken.value);
|
||||||
|
await writeFile({
|
||||||
|
value: appToken.value,
|
||||||
|
expiration: appToken.expiration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFile() {
|
||||||
|
return new Promise<any>((resolve, reject) => {
|
||||||
|
fs.readFile(
|
||||||
|
path.join(config.configPath, 'token.json'),
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
(err, data) => {
|
||||||
|
if (err) {
|
||||||
|
resolve({});
|
||||||
|
} else {
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeFile(data: any) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken();
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { CommandModule } from 'yargs';
|
||||||
|
export const updateCommand: CommandModule = {
|
||||||
|
command: 'update',
|
||||||
|
describe: 'Update and restart qinglong',
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs.option('repositority', {
|
||||||
|
type: 'string',
|
||||||
|
alias: 'r',
|
||||||
|
describe: `Specify the release warehouse address of the package`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv) => {},
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import * as yargs from 'yargs';
|
||||||
|
import { green, red } from 'chalk';
|
||||||
|
import { updateCommand } from './commands/update';
|
||||||
|
|
||||||
|
yargs
|
||||||
|
.usage('Usage: ql [command] <options>')
|
||||||
|
.command(updateCommand)
|
||||||
|
.fail((err) => {
|
||||||
|
console.error(`${red(err)}`);
|
||||||
|
})
|
||||||
|
.alias('h', 'help')
|
||||||
|
.showHelp()
|
||||||
|
.recommendCommands().argv;
|
||||||
+30
-12
@@ -1,14 +1,22 @@
|
|||||||
FROM node:lts-alpine
|
FROM node:alpine
|
||||||
LABEL maintainer="whyour"
|
|
||||||
ARG QL_URL=https://github.com/whyour/qinglong.git
|
ARG QL_MAINTAINER="whyour"
|
||||||
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
|
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 \$ " \
|
||||||
QL_DIR=/ql
|
QL_DIR=/ql \
|
||||||
|
QL_BRANCH=${QL_BRANCH}
|
||||||
|
|
||||||
WORKDIR ${QL_DIR}
|
WORKDIR ${QL_DIR}
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
|
||||||
|
RUN set -x \
|
||||||
|
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||||
&& apk update -f \
|
&& apk update -f \
|
||||||
&& apk upgrade \
|
&& apk upgrade \
|
||||||
&& apk --no-cache add -f bash \
|
&& apk --no-cache add -f bash \
|
||||||
@@ -24,19 +32,29 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
|||||||
python3 \
|
python3 \
|
||||||
jq \
|
jq \
|
||||||
openssh \
|
openssh \
|
||||||
|
py3-pip \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
|
&& apk update \
|
||||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||||
&& touch ~/.bashrc \
|
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||||
&& mkdir /run/nginx \
|
&& 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} \
|
&& 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 \
|
||||||
&& npm install -g pm2 \
|
|
||||||
&& npm install -g pnpm \
|
|
||||||
&& rm -rf /root/.npm \
|
|
||||||
&& 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/.npm \
|
||||||
|
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||||
|
&& mkdir -p ${QL_DIR}/static \
|
||||||
|
&& cp -rf /static/* ${QL_DIR}/static \
|
||||||
|
&& rm -rf /static
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -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
|
||||||
+28
-20
@@ -1,42 +1,50 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
|
||||||
|
|
||||||
dir_shell=/ql/shell
|
dir_shell=/ql/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
link_shell
|
link_shell
|
||||||
|
|
||||||
echo -e "======================1. 检测配置文件========================\n"
|
echo -e "======================1. 检测配置文件========================\n"
|
||||||
fix_config
|
make_dir /etc/nginx/conf.d
|
||||||
cp -fv $dir_root/docker/front.conf /etc/nginx/conf.d/front.conf
|
make_dir /run/nginx
|
||||||
|
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||||
|
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||||
|
pm2 l &>/dev/null
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo -e "======================2. 更新源代码========================\n"
|
echo -e "======================2. 安装依赖========================\n"
|
||||||
ql update "no-restart"
|
update_depend
|
||||||
pm2 l >/dev/null 2>&1
|
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo -e "======================3. 启动nginx========================\n"
|
echo -e "======================3. 启动nginx========================\n"
|
||||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||||
echo -e "nginx启动成功...\n"
|
echo -e "nginx启动成功...\n"
|
||||||
|
|
||||||
echo -e "======================4. 启动控制面板========================\n"
|
echo -e "======================4. 启动面板监控========================\n"
|
||||||
if [[ $(pm2 info panel 2>/dev/null) ]]; then
|
pm2 delete public &>/dev/null
|
||||||
pm2 reload panel
|
pm2 start $dir_static/build/public.js -n public --source-map-support --time
|
||||||
else
|
echo -e "监控服务启动成功...\n"
|
||||||
pm2 start $dir_root/build/app.js -n panel
|
|
||||||
fi
|
echo -e "======================5. 启动控制面板========================\n"
|
||||||
|
pm2 delete panel &>/dev/null
|
||||||
|
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
|
||||||
echo -e "控制面板启动成功...\n"
|
echo -e "控制面板启动成功...\n"
|
||||||
|
|
||||||
echo -e "======================5. 启动定时任务========================\n"
|
echo -e "======================6. 启动定时任务========================\n"
|
||||||
if [[ $(pm2 info schedule 2>/dev/null) ]]; then
|
pm2 delete schedule &>/dev/null
|
||||||
pm2 reload schedule
|
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule
|
|
||||||
fi
|
|
||||||
echo -e "定时任务启动成功...\n"
|
echo -e "定时任务启动成功...\n"
|
||||||
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
echo -e "======================6. 启动bot========================\n"
|
echo -e "======================7. 启动bot========================\n"
|
||||||
ql bot
|
nohup ql bot >>$dir_log/start.log 2>&1 &
|
||||||
|
echo -e "bot后台启动中...\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
|
echo -e "======================8. 执行自定义脚本========================\n"
|
||||||
|
nohup ql extra >>$dir_log/start.log 2>&1 &
|
||||||
|
echo -e "自定义脚本后台执行中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "############################################################\n"
|
echo -e "############################################################\n"
|
||||||
|
|||||||
+35
-2
@@ -1,13 +1,42 @@
|
|||||||
upstream api {
|
upstream api {
|
||||||
server localhost:5600;
|
server 0.0.0.0:5600;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream public {
|
||||||
|
server 0.0.0.0:5400;
|
||||||
|
}
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default keep-alive;
|
||||||
|
'websocket' 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 X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass http://api;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /open {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass http://api;
|
proxy_pass http://api;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,4 +53,8 @@ server {
|
|||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ .*\.(html)$ {
|
||||||
|
add_header Cache-Control no-cache;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
user root;
|
||||||
|
worker_processes auto;
|
||||||
|
pcre_jit on;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
include /etc/nginx/modules/*.conf;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
client_max_body_size 20m;
|
||||||
|
client_body_buffer_size 20m;
|
||||||
|
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
|
||||||
|
tcp_nodelay on;
|
||||||
|
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
ssl_session_cache shared:SSL:2m;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_static on;
|
||||||
|
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_buffers 16 8k;
|
||||||
|
gzip_http_version 1.0;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
+90
-43
@@ -1,14 +1,17 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "umi dev",
|
"start": "concurrently -n w: npm:start:*",
|
||||||
"build": "umi build",
|
"start:front": "umi dev",
|
||||||
"build-back": "tsc -p tsconfig.back.json",
|
"start:back": "nodemon",
|
||||||
"start-back": "nodemon",
|
"start:public": "ts-node back/public.ts",
|
||||||
"panel": "npm run build-back && node build/app.js",
|
"build:front": "umi build",
|
||||||
"schedule": "npm run build-back && node build/schedule.js",
|
"build:back": "tsc -p tsconfig.back.json",
|
||||||
"prepare": "umi generate tmp",
|
"panel": "npm run build:back && node static/build/app.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}'",
|
||||||
|
"postinstall": "umi generate tmp 2>/dev/null || true",
|
||||||
"test": "umi-test",
|
"test": "umi-test",
|
||||||
"test:coverage": "umi-test --coverage"
|
"test:coverage": "umi-test --coverage"
|
||||||
},
|
},
|
||||||
@@ -23,58 +26,102 @@
|
|||||||
"prettier --parser=typescript --write"
|
"prettier --parser=typescript --write"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"peerDependencyRules": {
|
||||||
|
"allowedVersions": {
|
||||||
|
"react": "17",
|
||||||
|
"react-dom": "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.19.0",
|
"@otplib/preset-default": "^12.0.1",
|
||||||
"celebrate": "^13.0.3",
|
"@sentry/node": "^7.0.0",
|
||||||
|
"@sentry/tracing": "^7.0.0",
|
||||||
|
"body-parser": "^1.19.2",
|
||||||
|
"celebrate": "^15.0.1",
|
||||||
|
"chokidar": "^3.5.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron-parser": "^3.5.0",
|
"cron-parser": "^4.2.1",
|
||||||
"dotenv": "^8.2.0",
|
"dayjs": "^1.11.2",
|
||||||
"express": "^4.17.1",
|
"dotenv": "^16.0.0",
|
||||||
"express-jwt": "^6.0.0",
|
"express": "^4.17.3",
|
||||||
|
"express-jwt": "^6.1.1",
|
||||||
|
"express-urlrewrite": "^1.4.0",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
|
"hpagent": "^0.1.2",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"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",
|
||||||
"p-queue": "6.6.2",
|
"p-queue": "7.2.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"typedi": "^0.8.0",
|
"sequelize": "^6.20.1",
|
||||||
"winston": "^3.3.3"
|
"serve-handler": "^6.1.3",
|
||||||
|
"sockjs": "^0.3.24",
|
||||||
|
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
|
||||||
|
"toad-scheduler": "^1.6.0",
|
||||||
|
"typedi": "^0.10.0",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
|
"winston": "^3.6.0",
|
||||||
|
"yargs": "^17.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/pro-layout": "^6.5.0",
|
"@ant-design/icons": "^4.7.0",
|
||||||
"@ant-design/icons": "^4.6.2",
|
"@ant-design/pro-layout": "^6.33.1",
|
||||||
"@types/cors": "^2.8.10",
|
"@monaco-editor/react": "^4.3.1",
|
||||||
"@types/express": "^4.17.8",
|
"@sentry/react": "^7.0.0",
|
||||||
"@types/express-jwt": "^6.0.1",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/nedb": "^1.8.11",
|
"@types/express": "^4.17.13",
|
||||||
"@types/node": "^14.11.2",
|
"@types/express-jwt": "^6.0.4",
|
||||||
"@types/node-fetch": "^2.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/qrcode.react": "^1.0.1",
|
"@types/lodash": "^4.14.179",
|
||||||
"@types/react": "^17.0.0",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/nedb": "^1.8.12",
|
||||||
"@umijs/plugin-antd": "^0.9.1",
|
"@types/node": "^17.0.21",
|
||||||
"@umijs/test": "^3.3.9",
|
"@types/node-schedule": "^1.3.2",
|
||||||
"codemirror": "^5.59.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"compression-webpack-plugin": "6.1.1",
|
"@types/qrcode.react": "^1.0.2",
|
||||||
"darkreader": "^4.9.27",
|
"@types/react": "^17.0.39",
|
||||||
"lint-staged": "^10.0.7",
|
"@types/react-dom": "^17.0.13",
|
||||||
"nodemon": "^2.0.4",
|
"@types/serve-handler": "^6.1.1",
|
||||||
"prettier": "^2.2.0",
|
"@types/sockjs": "^0.3.33",
|
||||||
|
"@types/sockjs-client": "^1.5.1",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
|
"@umijs/plugin-antd": "^0.15.0",
|
||||||
|
"@umijs/plugin-esbuild": "^1.4.1",
|
||||||
|
"@umijs/ssr-darkreader": "^4.9.45",
|
||||||
|
"@umijs/test": "^3.5.21",
|
||||||
|
"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",
|
||||||
|
"lint-staged": "^12.3.4",
|
||||||
|
"nodemon": "^2.0.15",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"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",
|
||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "^14.0.2",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^14.0.0",
|
||||||
"react-dom": "17.x",
|
"react-dom": "17.x",
|
||||||
"ts-node": "^9.0.0",
|
"react-split-pane": "^0.1.92",
|
||||||
"typescript": "^4.1.2",
|
"sockjs-client": "^1.6.0",
|
||||||
"umi": "^3.3.9",
|
"ts-node": "^10.6.0",
|
||||||
"umi-request": "^1.3.5",
|
"typescript": "^4.6.2",
|
||||||
|
"umi": "^3.5.21",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
-9826
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
@@ -1 +1 @@
|
|||||||
{ "username": "admin", "password": "adminadmin" }
|
{ "username": "admin", "password": "admin" }
|
||||||
|
|||||||
+31
-109
@@ -1,6 +1,6 @@
|
|||||||
## Version: v2.2.0-066
|
## Version: v2.8.0
|
||||||
## Date: 2021-06-17
|
## Date: 2021-06-20
|
||||||
## Update Content: \n1. 修复版本号样式\n2. 修复查看日志或者编辑任务后列表异常
|
## Update Content: 可持续发展纲要\n1. session管理破坏性修改\n2. 配置管理可编辑config下文件\n3. 自定义脚本改为查看脚本\n4. 移除互助相关
|
||||||
|
|
||||||
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
|
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
|
||||||
|
|
||||||
@@ -10,11 +10,14 @@ AutoDelCron="true"
|
|||||||
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
|
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
|
||||||
AutoAddCron="true"
|
AutoAddCron="true"
|
||||||
|
|
||||||
|
## 拉取脚本时默认的定时规则,当匹配不到定时规则时使用,例如: 0 9 * * *
|
||||||
|
DefaultCronRule=""
|
||||||
|
|
||||||
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
|
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
|
||||||
RepoFileExtensions="js py"
|
RepoFileExtensions="js py"
|
||||||
|
|
||||||
## 由于github仓库拉取较慢,所以会默认添加代理前缀,如不需要请移除
|
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
|
||||||
GithubProxyUrl="https://ghproxy.com/"
|
ProxyUrl=""
|
||||||
|
|
||||||
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||||
CommandTimeoutTime="1h"
|
CommandTimeoutTime="1h"
|
||||||
@@ -29,21 +32,14 @@ RandomDelay="300"
|
|||||||
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
|
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
|
||||||
EnableExtraShell="true"
|
EnableExtraShell="true"
|
||||||
|
|
||||||
## 自动按顺序进行账号间互助(选填) 设置为 true 时,将直接导入code最新日志来进行互助
|
|
||||||
AutoHelpOther=""
|
|
||||||
|
|
||||||
## 定义 jcode 脚本导出的互助码模板样式(选填)
|
|
||||||
## 不填 使用“按编号顺序助力模板”,Cookie编号在前的优先助力
|
|
||||||
## 填 0 使用“全部一致助力模板”,所有账户要助力的码全部一致
|
|
||||||
## 填 1 使用“均等机会助力模板”,所有账户获得助力次数一致
|
|
||||||
## 填 2 使用“随机顺序助力模板”,本套脚本内账号间随机顺序助力,每次生成的顺序都不一致。
|
|
||||||
HelpType=""
|
|
||||||
|
|
||||||
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
|
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
|
||||||
AutoStartBot=""
|
AutoStartBot=""
|
||||||
|
|
||||||
|
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
|
||||||
|
BotRepoUrl=""
|
||||||
|
|
||||||
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
||||||
PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
|
PipMirror="https://pypi.doubanio.com/simple/"
|
||||||
|
|
||||||
## 通知环境变量
|
## 通知环境变量
|
||||||
## 1. Server酱
|
## 1. Server酱
|
||||||
@@ -54,10 +50,14 @@ 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"
|
||||||
|
export BARK_GROUP="QingLong"
|
||||||
|
|
||||||
## 3. Telegram
|
## 3. Telegram
|
||||||
## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw
|
## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw
|
||||||
export TG_BOT_TOKEN=""
|
export TG_BOT_TOKEN=""
|
||||||
## 下方填写 @getuseridbot 中获取到的纯数字ID
|
## 下方填写 @getuseridbot 中获取到的纯数字ID
|
||||||
@@ -78,7 +78,7 @@ export TG_PROXY_AUTH=""
|
|||||||
## 如需使用,请赋值代理地址链接,并自行解除下一行的注释
|
## 如需使用,请赋值代理地址链接,并自行解除下一行的注释
|
||||||
export TG_API_HOST=""
|
export TG_API_HOST=""
|
||||||
|
|
||||||
## 4. 钉钉
|
## 4. 钉钉
|
||||||
## 官方文档:https://developers.dingtalk.com/document/app/custom-robot-access
|
## 官方文档:https://developers.dingtalk.com/document/app/custom-robot-access
|
||||||
## 下方填写token后面的内容,只需 https://oapi.dingtalk.com/robot/send?access_token=XXX 等于=符号后面的XXX即可
|
## 下方填写token后面的内容,只需 https://oapi.dingtalk.com/robot/send?access_token=XXX 等于=符号后面的XXX即可
|
||||||
export DD_BOT_TOKEN=""
|
export DD_BOT_TOKEN=""
|
||||||
@@ -108,103 +108,25 @@ export PUSH_PLUS_TOKEN=""
|
|||||||
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
|
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
|
||||||
export PUSH_PLUS_USER=""
|
export PUSH_PLUS_USER=""
|
||||||
|
|
||||||
## 8. go-cqhttp
|
## 9. go-cqhttp
|
||||||
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||||
## gobot_token 填写在go-cqhttp文件设置的访问密钥
|
## gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||||
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
||||||
## go-cqhttp相关API https://docs.go-cqhttp.org/api
|
## go-cqhttp相关API https://docs.go-cqhttp.org/api
|
||||||
export GOBOT_URL=""
|
export GOBOT_URL=""
|
||||||
export GOBOT_TOKEN=""
|
export GOBOT_TOKEN=""
|
||||||
export GOBOT_QQ=""
|
export GOBOT_QQ=""
|
||||||
|
|
||||||
## 如果只是想要屏蔽某个ck不执行某个脚本,可以参考下面 case 这个命令的例子来控制,脚本名称包含后缀
|
## 10. gotify
|
||||||
## case $1 in
|
## gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||||
## test.js)
|
## gotify_token 填写gotify的消息应用token
|
||||||
## TempBlockCookie="5"
|
## gotify_priority 填写推送消息优先级,默认为0
|
||||||
## ;;
|
export GOTIFY_URL=""
|
||||||
## esac
|
export GOTIFY_TOKEN=""
|
||||||
|
export GOTIFY_PRIORITY=0
|
||||||
|
|
||||||
## 需组合的环境变量列表,env_name需要和var_name一一对应,如何有新活动按照格式添加(不懂勿动)
|
## 11. PushDeer
|
||||||
env_name=(
|
## deer_key 填写PushDeer的key
|
||||||
JD_COOKIE
|
export DEER_KEY=""
|
||||||
FRUITSHARECODES
|
|
||||||
PETSHARECODES
|
|
||||||
PLANT_BEAN_SHARECODES
|
|
||||||
DREAM_FACTORY_SHARE_CODES
|
|
||||||
DDFACTORY_SHARECODES
|
|
||||||
JDZZ_SHARECODES
|
|
||||||
JDJOY_SHARECODES
|
|
||||||
JXNC_SHARECODES
|
|
||||||
BOOKSHOP_SHARECODES
|
|
||||||
JD_CASH_SHARECODES
|
|
||||||
JDSGMH_SHARECODES
|
|
||||||
JDCFD_SHARECODES
|
|
||||||
JDHEALTH_SHARECODES
|
|
||||||
)
|
|
||||||
var_name=(
|
|
||||||
Cookie
|
|
||||||
ForOtherFruit
|
|
||||||
ForOtherPet
|
|
||||||
ForOtherBean
|
|
||||||
ForOtherDreamFactory
|
|
||||||
ForOtherJdFactory
|
|
||||||
ForOtherJdzz
|
|
||||||
ForOtherJoy
|
|
||||||
ForOtherJxnc
|
|
||||||
ForOtherBookShop
|
|
||||||
ForOtherCash
|
|
||||||
ForOtherSgmh
|
|
||||||
ForOtherCfd
|
|
||||||
ForOtherHealth
|
|
||||||
)
|
|
||||||
|
|
||||||
## name_js为脚本文件名,如果使用ql repo命令拉取,文件名含有作者名
|
|
||||||
## 所有有互助码的活动,把脚本名称列在 name_js 中,对应 config.sh 中互助码后缀列在 name_config 中,中文名称列在 name_chinese 中。
|
|
||||||
## name_js、name_config 和 name_chinese 中的三个名称必须一一对应。
|
|
||||||
name_js=(
|
|
||||||
jd_fruit
|
|
||||||
jd_pet
|
|
||||||
jd_plantBean
|
|
||||||
jd_dreamFactory
|
|
||||||
jd_jdfactory
|
|
||||||
jd_jdzz
|
|
||||||
jd_crazy_joy
|
|
||||||
jd_jxnc
|
|
||||||
jd_bookshop
|
|
||||||
jd_cash
|
|
||||||
jd_sgmh
|
|
||||||
jd_cfd
|
|
||||||
jd_health
|
|
||||||
)
|
|
||||||
name_config=(
|
|
||||||
Fruit
|
|
||||||
Pet
|
|
||||||
Bean
|
|
||||||
DreamFactory
|
|
||||||
JdFactory
|
|
||||||
Jdzz
|
|
||||||
Joy
|
|
||||||
Jxnc
|
|
||||||
BookShop
|
|
||||||
Cash
|
|
||||||
Sgmh
|
|
||||||
Cfd
|
|
||||||
Health
|
|
||||||
)
|
|
||||||
name_chinese=(
|
|
||||||
东东农场
|
|
||||||
东东萌宠
|
|
||||||
京东种豆得豆
|
|
||||||
京喜工厂
|
|
||||||
东东工厂
|
|
||||||
京东赚赚
|
|
||||||
crazyJoy任务
|
|
||||||
京喜农场
|
|
||||||
口袋书店
|
|
||||||
签到领现金
|
|
||||||
闪购盲盒
|
|
||||||
京喜财富岛
|
|
||||||
东东健康社区
|
|
||||||
)
|
|
||||||
|
|
||||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 添加你需要重启自动执行的任意命令,比如 ql repo
|
## 添加你需要重启自动执行的任意命令,比如 ql repo
|
||||||
|
## 安装node依赖使用 pnpm add -g xxx xxx
|
||||||
|
## 安装python依赖使用 pip3 install xxx
|
||||||
|
|||||||
+144
-11
@@ -13,25 +13,41 @@
|
|||||||
const querystring = require('querystring');
|
const querystring = require('querystring');
|
||||||
const $ = new Env();
|
const $ = new Env();
|
||||||
const timeout = 15000; //超时时间(单位毫秒)
|
const timeout = 15000; //超时时间(单位毫秒)
|
||||||
|
// =======================================gotify通知设置区域==============================================
|
||||||
|
//gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||||
|
//gotify_token 填写gotify的消息应用token
|
||||||
|
//gotify_priority 填写推送消息优先级,默认为0
|
||||||
|
let GOTIFY_URL = '';
|
||||||
|
let GOTIFY_TOKEN = '';
|
||||||
|
let GOTIFY_PRIORITY = 0;
|
||||||
// =======================================go-cqhttp通知设置区域===========================================
|
// =======================================go-cqhttp通知设置区域===========================================
|
||||||
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg
|
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg
|
||||||
//gobot_token 填写在go-cqhttp文件设置的访问密钥
|
//gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||||
//gobot_qq 填写推送到个人QQ或者QQ群号
|
//gobot_qq 填写推送到个人QQ或者QQ群号
|
||||||
//go-cqhttp相关API https://docs.go-cqhttp.org/api
|
//go-cqhttp相关API https://docs.go-cqhttp.org/api
|
||||||
let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||||
let GOBOT_TOKEN = ''; //访问密钥
|
let GOBOT_TOKEN = ''; //访问密钥
|
||||||
let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
||||||
|
|
||||||
// =======================================微信server酱通知设置区域===========================================
|
// =======================================微信server酱通知设置区域===========================================
|
||||||
//此处填你申请的SCKEY.
|
//此处填你申请的SCKEY.
|
||||||
//(环境变量名 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"
|
||||||
|
let BARK_GROUP = 'QingLong';
|
||||||
|
|
||||||
// =======================================telegram机器人通知设置区域===========================================
|
// =======================================telegram机器人通知设置区域===========================================
|
||||||
//此处填你telegram bot 的Token,telegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
|
//此处填你telegram bot 的Token,telegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
|
||||||
@@ -81,7 +97,24 @@ 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) {
|
||||||
|
GOTIFY_URL = process.env.GOTIFY_URL;
|
||||||
|
}
|
||||||
|
if (process.env.GOTIFY_TOKEN) {
|
||||||
|
GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
|
||||||
|
}
|
||||||
|
if (process.env.GOTIFY_PRIORITY) {
|
||||||
|
GOTIFY_PRIORITY = process.env.GOTIFY_PRIORITY;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.GOBOT_URL) {
|
if (process.env.GOBOT_URL) {
|
||||||
GOBOT_URL = process.env.GOBOT_URL;
|
GOBOT_URL = process.env.GOBOT_URL;
|
||||||
}
|
}
|
||||||
@@ -96,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;
|
||||||
}
|
}
|
||||||
@@ -114,9 +151,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
if (process.env.BARK_GROUP) {
|
||||||
|
BARK_GROUP = process.env.BARK_GROUP;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
BARK_PUSH &&
|
BARK_PUSH &&
|
||||||
@@ -194,18 +237,56 @@ async function sendNotify(
|
|||||||
qywxBotNotify(text, desp), //企业微信机器人
|
qywxBotNotify(text, desp), //企业微信机器人
|
||||||
qywxamNotify(text, desp), //企业微信应用消息推送
|
qywxamNotify(text, desp), //企业微信应用消息推送
|
||||||
iGotNotify(text, desp, params), //iGot
|
iGotNotify(text, desp, params), //iGot
|
||||||
gobotNotify(text, desp),//go-cqhttp
|
gobotNotify(text, desp), //go-cqhttp
|
||||||
|
gotifyNotify(text, desp), //gotify
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gotifyNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (GOTIFY_URL && GOTIFY_TOKEN) {
|
||||||
|
const options = {
|
||||||
|
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
|
||||||
|
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(
|
||||||
|
desp,
|
||||||
|
)}&priority=${GOTIFY_PRIORITY}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('gotify发送通知调用API失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.id) {
|
||||||
|
console.log('gotify发送通知消息成功🎉\n');
|
||||||
|
} else {
|
||||||
|
console.log(`${data.message}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function gobotNotify(text, desp, time = 2100) {
|
function gobotNotify(text, desp, time = 2100) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (GOBOT_URL) {
|
if (GOBOT_URL) {
|
||||||
const options = {
|
const options = {
|
||||||
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
|
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
|
||||||
body: `message=${text}\n${desp}`,
|
json: { message: `${text}\n${desp}` },
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
timeout,
|
timeout,
|
||||||
};
|
};
|
||||||
@@ -288,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) {
|
||||||
@@ -371,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}&${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',
|
||||||
},
|
},
|
||||||
@@ -407,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,
|
||||||
};
|
};
|
||||||
@@ -597,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]) {
|
||||||
|
|||||||
+528
-240
@@ -1,277 +1,565 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# _*_ coding:utf-8 _*_
|
# _*_ coding:utf-8 _*_
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
cur_path = os.path.abspath(os.path.dirname(__file__))
|
|
||||||
root_path = os.path.split(cur_path)[0]
|
|
||||||
sys.path.append(root_path)
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import traceback
|
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
import base64
|
import base64
|
||||||
import urllib.parse
|
import hashlib
|
||||||
from requests.adapters import HTTPAdapter
|
import hmac
|
||||||
from urllib3.util import Retry
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# 原先的 print 函数和主线程的锁
|
||||||
|
_print = print
|
||||||
|
mutex = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
# 定义新的 print 函数
|
||||||
|
def print(text, *args, **kw):
|
||||||
|
"""
|
||||||
|
使输出有序进行,不出现多线程同一时间输出导致错乱的问题。
|
||||||
|
"""
|
||||||
|
with mutex:
|
||||||
|
_print(text, *args, **kw)
|
||||||
|
|
||||||
|
|
||||||
# 通知服务
|
# 通知服务
|
||||||
BARK = '' # bark服务,此参数如果以http或者https开头则判定为自建bark服务; secrets可填;
|
# fmt: off
|
||||||
SCKEY = '' # Server酱的SCKEY; secrets可填
|
push_config = {
|
||||||
TG_BOT_TOKEN = '' # tg机器人的TG_BOT_TOKEN; secrets可填
|
'HITOKOTO': False, # 启用一言(随机句子)
|
||||||
TG_USER_ID = '' # tg机器人的TG_USER_ID; secrets可填
|
|
||||||
TG_PROXY_IP = '' # tg机器人的TG_PROXY_IP; secrets可填
|
|
||||||
TG_PROXY_PORT = '' # tg机器人的TG_PROXY_PORT; secrets可填
|
|
||||||
DD_BOT_ACCESS_TOKEN = '' # 钉钉机器人的DD_BOT_ACCESS_TOKEN; secrets可填
|
|
||||||
DD_BOT_SECRET = '' # 钉钉机器人的DD_BOT_SECRET; secrets可填
|
|
||||||
QYWX_APP = '' # 企业微信应用的QYWX_APP; secrets可填 参考http://note.youdao.com/s/HMiudGkb
|
|
||||||
|
|
||||||
notify_mode = []
|
'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
|
||||||
|
'BARK_ARCHIVE': '', # bark 推送是否存档
|
||||||
|
'BARK_GROUP': '', # bark 推送分组
|
||||||
|
'BARK_SOUND': '', # bark 推送声音
|
||||||
|
'BARK_ICON': '', # bark 推送图标
|
||||||
|
|
||||||
# GitHub action运行需要填写对应的secrets
|
'CONSOLE': True, # 控制台输出
|
||||||
if "BARK" in os.environ and os.environ["BARK"]:
|
|
||||||
BARK = os.environ["BARK"]
|
|
||||||
if "SCKEY" in os.environ and os.environ["SCKEY"]:
|
|
||||||
SCKEY = os.environ["SCKEY"]
|
|
||||||
if "TG_BOT_TOKEN" in os.environ and os.environ["TG_BOT_TOKEN"] and "TG_USER_ID" in os.environ and os.environ["TG_USER_ID"]:
|
|
||||||
TG_BOT_TOKEN = os.environ["TG_BOT_TOKEN"]
|
|
||||||
TG_USER_ID = os.environ["TG_USER_ID"]
|
|
||||||
if "DD_BOT_ACCESS_TOKEN" in os.environ and os.environ["DD_BOT_ACCESS_TOKEN"] and "DD_BOT_SECRET" in os.environ and os.environ["DD_BOT_SECRET"]:
|
|
||||||
DD_BOT_ACCESS_TOKEN = os.environ["DD_BOT_ACCESS_TOKEN"]
|
|
||||||
DD_BOT_SECRET = os.environ["DD_BOT_SECRET"]
|
|
||||||
if "QYWX_APP" in os.environ and os.environ["QYWX_APP"]:
|
|
||||||
QYWX_APP = os.environ["QYWX_APP"]
|
|
||||||
|
|
||||||
if BARK:
|
'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
|
||||||
notify_mode.append('bark')
|
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
|
||||||
print("BARK 推送打开")
|
|
||||||
if SCKEY:
|
|
||||||
notify_mode.append('sc_key')
|
|
||||||
print("Server酱 推送打开")
|
|
||||||
if TG_BOT_TOKEN and TG_USER_ID:
|
|
||||||
notify_mode.append('telegram_bot')
|
|
||||||
print("Telegram 推送打开")
|
|
||||||
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET:
|
|
||||||
notify_mode.append('dingding_bot')
|
|
||||||
print("钉钉机器人 推送打开")
|
|
||||||
if QYWX_APP:
|
|
||||||
notify_mode.append('qywxapp_bot')
|
|
||||||
print("企业微信应用 推送打开")
|
|
||||||
|
|
||||||
def bark(title, content):
|
'FSKEY': '', # 飞书机器人的 FSKEY
|
||||||
print("\n")
|
|
||||||
if not BARK:
|
'GOBOT_URL': '', # go-cqhttp
|
||||||
print("bark服务的bark_token未设置!!\n取消推送")
|
# 推送到个人QQ:http://127.0.0.1/send_private_msg
|
||||||
|
# 群:http://127.0.0.1/send_group_msg
|
||||||
|
'GOBOT_QQ': '', # go-cqhttp 的推送群或用户
|
||||||
|
# GOBOT_URL 设置 /send_private_msg 时填入 user_id=个人QQ
|
||||||
|
# /send_group_msg 时填入 group_id=QQ群
|
||||||
|
'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
|
||||||
|
|
||||||
|
'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
|
||||||
|
'GOTIFY_TOKEN': '', # gotify的消息应用token
|
||||||
|
'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
|
||||||
|
|
||||||
|
'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
|
||||||
|
|
||||||
|
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
||||||
|
|
||||||
|
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
|
||||||
|
|
||||||
|
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||||
|
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||||
|
|
||||||
|
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
|
||||||
|
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
||||||
|
|
||||||
|
'QYWX_AM': '', # 企业微信应用
|
||||||
|
|
||||||
|
'QYWX_KEY': '', # 企业微信机器人
|
||||||
|
|
||||||
|
'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ
|
||||||
|
'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1434078534
|
||||||
|
'TG_API_HOST': '', # tg 代理 api
|
||||||
|
'TG_PROXY_AUTH': '', # tg 代理认证参数
|
||||||
|
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
|
||||||
|
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
|
||||||
|
}
|
||||||
|
notify_function = []
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
# 首先读取 面板变量 或者 github action 运行变量
|
||||||
|
for k in push_config:
|
||||||
|
if os.getenv(k):
|
||||||
|
v = os.getenv(k)
|
||||||
|
push_config[k] = v
|
||||||
|
|
||||||
|
|
||||||
|
def bark(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 bark 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("BARK_PUSH"):
|
||||||
|
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("bark服务启动")
|
print("bark 服务启动")
|
||||||
url = None
|
|
||||||
if BARK.startswith('http'):
|
if push_config.get("BARK_PUSH").startswith("http"):
|
||||||
url = f"""{BARK}/{title}/{content}"""
|
url = f'{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
|
||||||
else:
|
else:
|
||||||
url = f"""https://api.day.app/{BARK}/{title}/{content}"""
|
url = f'https://api.day.app/{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
|
||||||
|
|
||||||
|
bark_params = {
|
||||||
|
"BARK_ARCHIVE": "isArchive",
|
||||||
|
"BARK_GROUP": "group",
|
||||||
|
"BARK_SOUND": "sound",
|
||||||
|
"BARK_ICON": "icon",
|
||||||
|
}
|
||||||
|
params = ""
|
||||||
|
for pair in filter(
|
||||||
|
lambda pairs: pairs[0].startswith("BARK_")
|
||||||
|
and pairs[0] != "BARK_PUSH"
|
||||||
|
and pairs[1]
|
||||||
|
and bark_params.get(pairs[0]),
|
||||||
|
push_config.items(),
|
||||||
|
):
|
||||||
|
params += f"{bark_params.get(pair[0])}={pair[1]}&"
|
||||||
|
if params:
|
||||||
|
url = url + "?" + params.rstrip("&")
|
||||||
response = requests.get(url).json()
|
response = requests.get(url).json()
|
||||||
if response['code'] == 200:
|
|
||||||
print('推送成功!')
|
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def serverJ(title, content):
|
if response["code"] == 200:
|
||||||
print("\n")
|
print("bark 推送成功!")
|
||||||
if not SCKEY:
|
else:
|
||||||
print("server酱服务的SCKEY未设置!!\n取消推送")
|
print("bark 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def console(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 控制台 推送消息。
|
||||||
|
"""
|
||||||
|
print(f"{title}\n\n{content}")
|
||||||
|
|
||||||
|
|
||||||
|
def dingding_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 钉钉机器人 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
|
||||||
|
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("serverJ服务启动")
|
print("钉钉机器人 服务启动")
|
||||||
|
|
||||||
|
timestamp = str(round(time.time() * 1000))
|
||||||
|
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
|
||||||
|
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
|
||||||
|
string_to_sign_enc = string_to_sign.encode("utf-8")
|
||||||
|
hmac_code = hmac.new(
|
||||||
|
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
|
||||||
|
).digest()
|
||||||
|
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
||||||
|
url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get("DD_BOT_TOKEN")}×tamp={timestamp}&sign={sign}'
|
||||||
|
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||||
|
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||||
|
response = requests.post(
|
||||||
|
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||||
|
).json()
|
||||||
|
|
||||||
|
if not response["errcode"]:
|
||||||
|
print("钉钉机器人 推送成功!")
|
||||||
|
else:
|
||||||
|
print("钉钉机器人 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def feishu_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 飞书机器人 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("FSKEY"):
|
||||||
|
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("飞书 服务启动")
|
||||||
|
|
||||||
|
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
|
||||||
|
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
|
||||||
|
response = requests.post(url, data=json.dumps(data)).json()
|
||||||
|
|
||||||
|
if response.get("StatusCode") == 0:
|
||||||
|
print("飞书 推送成功!")
|
||||||
|
else:
|
||||||
|
print("飞书 推送失败!错误信息如下:\n", response)
|
||||||
|
|
||||||
|
|
||||||
|
def go_cqhttp(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 go_cqhttp 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
|
||||||
|
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("go-cqhttp 服务启动")
|
||||||
|
|
||||||
|
url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
|
||||||
|
response = requests.get(url).json()
|
||||||
|
|
||||||
|
if response["status"] == "ok":
|
||||||
|
print("go-cqhttp 推送成功!")
|
||||||
|
else:
|
||||||
|
print("go-cqhttp 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def gotify(title:str,content:str) -> None:
|
||||||
|
"""
|
||||||
|
使用 gotify 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
||||||
|
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("gotify 服务启动")
|
||||||
|
|
||||||
|
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
|
||||||
|
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
|
||||||
|
response = requests.post(url,data=data).json()
|
||||||
|
|
||||||
|
if response.get("id"):
|
||||||
|
print("gotify 推送成功!")
|
||||||
|
else:
|
||||||
|
print("gotify 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def iGot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 iGot 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("IGOT_PUSH_KEY"):
|
||||||
|
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("iGot 服务启动")
|
||||||
|
|
||||||
|
url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
|
||||||
|
data = {"title": title, "content": content}
|
||||||
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
|
response = requests.post(url, data=data, headers=headers).json()
|
||||||
|
|
||||||
|
if response["ret"] == 0:
|
||||||
|
print("iGot 推送成功!")
|
||||||
|
else:
|
||||||
|
print(f'iGot 推送失败!{response["errMsg"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def serverJ(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 serverJ 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("PUSH_KEY"):
|
||||||
|
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("serverJ 服务启动")
|
||||||
|
|
||||||
|
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
||||||
|
if push_config.get("PUSH_KEY").index("SCT") != -1:
|
||||||
|
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||||
|
else:
|
||||||
|
url = f'https://sc.ftqq.com/${push_config.get("PUSH_KEY")}.send'
|
||||||
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
|
if response.get("errno") == 0 or response.get("code") == 0:
|
||||||
|
print("serverJ 推送成功!")
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
通过 push+ 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
|
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("PUSHPLUS 服务启动")
|
||||||
|
|
||||||
|
url = "http://www.pushplus.plus/send"
|
||||||
data = {
|
data = {
|
||||||
"text": title,
|
"token": push_config.get("PUSH_PLUS_TOKEN"),
|
||||||
"desp": content.replace("\n", "\n\n")
|
"title": title,
|
||||||
|
"content": content,
|
||||||
|
"topic": push_config.get("PUSH_PLUS_USER"),
|
||||||
}
|
}
|
||||||
response = requests.post(f"https://sc.ftqq.com/{SCKEY}.send", data=data).json()
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
if response['errno'] == 0:
|
headers = {"Content-Type": "application/json"}
|
||||||
print('推送成功!')
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def telegram_bot(title, content):
|
if response["code"] == 200:
|
||||||
print("\n")
|
print("PUSHPLUS 推送成功!")
|
||||||
bot_token = TG_BOT_TOKEN
|
|
||||||
user_id = TG_USER_ID
|
else:
|
||||||
if not bot_token or not user_id:
|
|
||||||
print("tg服务的bot_token或者user_id未设置!!\n取消推送")
|
url_old = "http://pushplus.hxtrip.com/send"
|
||||||
|
headers["Accept"] = "application/json"
|
||||||
|
response = requests.post(url=url_old, data=body, headers=headers).json()
|
||||||
|
|
||||||
|
if response["code"] == 200:
|
||||||
|
print("PUSHPLUS(hxtrip) 推送成功!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("PUSHPLUS 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def qmsg_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 qmsg 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
|
||||||
|
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("tg服务启动")
|
print("qmsg 服务启动")
|
||||||
url=f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage"
|
|
||||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
||||||
payload = {'chat_id': str(TG_USER_ID), 'text': f'{title}\n\n{content}', 'disable_web_page_preview': 'true'}
|
|
||||||
proxies = None
|
|
||||||
if TG_PROXY_IP and TG_PROXY_PORT:
|
|
||||||
proxyStr = "http://{}:{}".format(TG_PROXY_IP, TG_PROXY_PORT)
|
|
||||||
proxies = {"http": proxyStr, "https": proxyStr}
|
|
||||||
response = requests.post(url=url, headers=headers, params=payload, proxies=proxies).json()
|
|
||||||
if response['ok']:
|
|
||||||
print('推送成功!')
|
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def dingding_bot(title, content):
|
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
|
||||||
timestamp = str(round(time.time() * 1000)) # 时间戳
|
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
|
||||||
secret_enc = DD_BOT_SECRET.encode('utf-8')
|
response = requests.post(url=url, params=payload).json()
|
||||||
string_to_sign = '{}\n{}'.format(timestamp, DD_BOT_SECRET)
|
|
||||||
string_to_sign_enc = string_to_sign.encode('utf-8')
|
|
||||||
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
|
|
||||||
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) # 签名
|
|
||||||
print('开始使用 钉钉机器人 推送消息...', end='')
|
|
||||||
url = f'https://oapi.dingtalk.com/robot/send?access_token={DD_BOT_ACCESS_TOKEN}×tamp={timestamp}&sign={sign}'
|
|
||||||
headers = {'Content-Type': 'application/json;charset=utf-8'}
|
|
||||||
data = {
|
|
||||||
'msgtype': 'text',
|
|
||||||
'text': {'content': f'{title}\n\n{content}'}
|
|
||||||
}
|
|
||||||
response = requests.post(url=url, data=json.dumps(data), headers=headers, timeout=15).json()
|
|
||||||
if not response['errcode']:
|
|
||||||
print('推送成功!')
|
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def qywxapp_bot(title, content):
|
if response["code"] == 0:
|
||||||
print("\n")
|
print("qmsg 推送成功!")
|
||||||
if not QYWX_APP:
|
else:
|
||||||
print("企业微信应用的QYWX_APP未设置!!\n取消推送")
|
print(f'qmsg 推送失败!{response["reason"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def wecom_app(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 企业微信 APP 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("QYWX_AM"):
|
||||||
|
print("QYWX_AM 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("企业微信应用启动")
|
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
||||||
qywx_app_params = QYWX_APP.split(',')
|
if 4 < len(QYWX_AM_AY) > 5:
|
||||||
url='https://qyapi.weixin.qq.com/cgi-bin/gettoken'
|
print("QYWX_AM 设置错误!!\n取消推送")
|
||||||
headers= {
|
return
|
||||||
'Content-Type': 'application/json',
|
print("企业微信 APP 服务启动")
|
||||||
}
|
|
||||||
payload = {
|
|
||||||
'corpid': qywx_app_params[0],
|
|
||||||
'corpsecret': qywx_app_params[1],
|
|
||||||
}
|
|
||||||
response = requests.post(url=url, headers=headers, data=json.dumps(payload), timeout=15).json()
|
|
||||||
accesstoken = response["access_token"]
|
|
||||||
html = content.replace("\n", "<br/>")
|
|
||||||
|
|
||||||
options = None
|
corpid = QYWX_AM_AY[0]
|
||||||
if not qywx_app_params[4]:
|
corpsecret = QYWX_AM_AY[1]
|
||||||
options = {
|
touser = QYWX_AM_AY[2]
|
||||||
'msgtype': 'text',
|
agentid = QYWX_AM_AY[3]
|
||||||
'text': {
|
try:
|
||||||
content: f'{title}\n\n${content}'
|
media_id = QYWX_AM_AY[4]
|
||||||
}
|
except IndexError:
|
||||||
}
|
media_id = ""
|
||||||
elif qywx_app_params[4] == '0':
|
wx = WeCom(corpid, corpsecret, agentid)
|
||||||
options = {
|
# 如果没有配置 media_id 默认就以 text 方式发送
|
||||||
'msgtype': 'textcard',
|
if not media_id:
|
||||||
'textcard': {
|
message = title + "\n\n" + content
|
||||||
title: f'{title}',
|
response = wx.send_text(message, touser)
|
||||||
description: f'{content}',
|
|
||||||
btntxt: '更多'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elif qywx_app_params[4] == '1':
|
|
||||||
options = {
|
|
||||||
'msgtype': 'text',
|
|
||||||
'text': {
|
|
||||||
content: f'{title}\n\n${content}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
options = {
|
response = wx.send_mpnews(title, content, media_id, touser)
|
||||||
'msgtype': 'mpnews',
|
|
||||||
'mpnews': {
|
if response == "ok":
|
||||||
'articles': [
|
print("企业微信推送成功!")
|
||||||
|
else:
|
||||||
|
print("企业微信推送失败!错误信息如下:\n", response)
|
||||||
|
|
||||||
|
|
||||||
|
class WeCom:
|
||||||
|
def __init__(self, corpid, corpsecret, agentid):
|
||||||
|
self.CORPID = corpid
|
||||||
|
self.CORPSECRET = corpsecret
|
||||||
|
self.AGENTID = agentid
|
||||||
|
|
||||||
|
def get_access_token(self):
|
||||||
|
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||||
|
values = {
|
||||||
|
"corpid": self.CORPID,
|
||||||
|
"corpsecret": self.CORPSECRET,
|
||||||
|
}
|
||||||
|
req = requests.post(url, params=values)
|
||||||
|
data = json.loads(req.text)
|
||||||
|
return data["access_token"]
|
||||||
|
|
||||||
|
def send_text(self, message, touser="@all"):
|
||||||
|
send_url = (
|
||||||
|
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
|
||||||
|
+ self.get_access_token()
|
||||||
|
)
|
||||||
|
send_values = {
|
||||||
|
"touser": touser,
|
||||||
|
"msgtype": "text",
|
||||||
|
"agentid": self.AGENTID,
|
||||||
|
"text": {"content": message},
|
||||||
|
"safe": "0",
|
||||||
|
}
|
||||||
|
send_msges = bytes(json.dumps(send_values), "utf-8")
|
||||||
|
respone = requests.post(send_url, send_msges)
|
||||||
|
respone = respone.json()
|
||||||
|
return respone["errmsg"]
|
||||||
|
|
||||||
|
def send_mpnews(self, title, message, media_id, touser="@all"):
|
||||||
|
send_url = (
|
||||||
|
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
|
||||||
|
+ self.get_access_token()
|
||||||
|
)
|
||||||
|
send_values = {
|
||||||
|
"touser": touser,
|
||||||
|
"msgtype": "mpnews",
|
||||||
|
"agentid": self.AGENTID,
|
||||||
|
"mpnews": {
|
||||||
|
"articles": [
|
||||||
{
|
{
|
||||||
'title': f'{title}',
|
"title": title,
|
||||||
'thumb_media_id': f'{qywx_app_params[4]}',
|
"thumb_media_id": media_id,
|
||||||
'author': '智能助手',
|
"author": "Author",
|
||||||
'content_source_url': '',
|
"content_source_url": "",
|
||||||
'content': f'{html}',
|
"content": message.replace("\n", "<br/>"),
|
||||||
'digest': f'{content}'
|
"digest": message,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
send_msges = bytes(json.dumps(send_values), "utf-8")
|
||||||
|
respone = requests.post(send_url, send_msges)
|
||||||
|
respone = respone.json()
|
||||||
|
return respone["errmsg"]
|
||||||
|
|
||||||
url=f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={accesstoken}"
|
|
||||||
data = {
|
|
||||||
'touser': f'{change_user_id(content)}',
|
|
||||||
'agentid': f'{qywx_app_params[3]}',
|
|
||||||
'safe': '0'
|
|
||||||
}
|
|
||||||
data.update(options)
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
response = requests.post(url=url, headers=headers, data=json.dumps(data)).json()
|
|
||||||
|
|
||||||
if response['errcode'] == 0:
|
def wecom_bot(title: str, content: str) -> None:
|
||||||
print('推送成功!')
|
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def change_user_id(desp):
|
|
||||||
qywx_app_params = QYWX_APP.split(',')
|
|
||||||
if qywx_app_params[2]:
|
|
||||||
userIdTmp = qywx_app_params[2].split("|")
|
|
||||||
userId = ""
|
|
||||||
for i in range(len(userIdTmp)):
|
|
||||||
count1 = f"账号{i + 1}"
|
|
||||||
count2 = f"签到号{i + 1}"
|
|
||||||
if re.search(count1, desp) or re.search(count2, desp):
|
|
||||||
userId = userIdTmp[i]
|
|
||||||
if not userId:
|
|
||||||
userId = qywx_app_params[2]
|
|
||||||
return userId
|
|
||||||
else:
|
|
||||||
return "@all"
|
|
||||||
|
|
||||||
def send(title, content):
|
|
||||||
"""
|
"""
|
||||||
使用 bark, telegram bot, dingding bot, serverJ 发送手机推送
|
通过 企业微信机器人 推送消息。
|
||||||
:param title:
|
"""
|
||||||
:param content:
|
if not push_config.get("QYWX_KEY"):
|
||||||
|
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("企业微信机器人服务启动")
|
||||||
|
|
||||||
|
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
|
||||||
|
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||||
|
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||||
|
response = requests.post(
|
||||||
|
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||||
|
).json()
|
||||||
|
|
||||||
|
if response["errcode"] == 0:
|
||||||
|
print("企业微信机器人推送成功!")
|
||||||
|
else:
|
||||||
|
print("企业微信机器人推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def telegram_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 telegram 机器人 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
|
||||||
|
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("tg 服务启动")
|
||||||
|
|
||||||
|
if push_config.get("TG_API_HOST"):
|
||||||
|
url = f"https://{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||||
|
else:
|
||||||
|
url = (
|
||||||
|
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||||
|
)
|
||||||
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
|
payload = {
|
||||||
|
"chat_id": str(push_config.get("TG_USER_ID")),
|
||||||
|
"text": f"{title}\n\n{content}",
|
||||||
|
"disable_web_page_preview": "true",
|
||||||
|
}
|
||||||
|
proxies = None
|
||||||
|
if push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"):
|
||||||
|
if push_config.get("TG_PROXY_AUTH") is not None and "@" not in push_config.get(
|
||||||
|
"TG_PROXY_HOST"
|
||||||
|
):
|
||||||
|
push_config["TG_PROXY_HOST"] = (
|
||||||
|
push_config.get("TG_PROXY_AUTH")
|
||||||
|
+ "@"
|
||||||
|
+ push_config.get("TG_PROXY_HOST")
|
||||||
|
)
|
||||||
|
proxyStr = "http://{}:{}".format(
|
||||||
|
push_config.get("TG_PROXY_HOST"), push_config.get("TG_PROXY_PORT")
|
||||||
|
)
|
||||||
|
proxies = {"http": proxyStr, "https": proxyStr}
|
||||||
|
response = requests.post(
|
||||||
|
url=url, headers=headers, params=payload, proxies=proxies
|
||||||
|
).json()
|
||||||
|
|
||||||
|
if response["ok"]:
|
||||||
|
print("tg 推送成功!")
|
||||||
|
else:
|
||||||
|
print("tg 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def one() -> str:
|
||||||
|
"""
|
||||||
|
获取一条一言。
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
for i in notify_mode:
|
url = "https://v1.hitokoto.cn/"
|
||||||
if i == 'bark':
|
res = requests.get(url).json()
|
||||||
if BARK:
|
return res["hitokoto"] + " ----" + res["from"]
|
||||||
bark(title=title, content=content)
|
|
||||||
else:
|
|
||||||
print('未启用 bark')
|
if push_config.get("BARK_PUSH"):
|
||||||
continue
|
notify_function.append(bark)
|
||||||
if i == 'sc_key':
|
if push_config.get("CONSOLE"):
|
||||||
if SCKEY:
|
notify_function.append(console)
|
||||||
serverJ(title=title, content=content)
|
if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
|
||||||
else:
|
notify_function.append(dingding_bot)
|
||||||
print('未启用 Server酱')
|
if push_config.get("FSKEY"):
|
||||||
continue
|
notify_function.append(feishu_bot)
|
||||||
elif i == 'dingding_bot':
|
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
|
||||||
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET:
|
notify_function.append(go_cqhttp)
|
||||||
dingding_bot(title=title, content=content)
|
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
|
||||||
else:
|
notify_function.append(gotify)
|
||||||
print('未启用 钉钉机器人')
|
if push_config.get("IGOT_PUSH_KEY"):
|
||||||
continue
|
notify_function.append(iGot)
|
||||||
elif i == 'telegram_bot':
|
if push_config.get("PUSH_KEY"):
|
||||||
if TG_BOT_TOKEN and TG_USER_ID:
|
notify_function.append(serverJ)
|
||||||
telegram_bot(title=title, content=content)
|
if push_config.get("DEER_KEY"):
|
||||||
else:
|
notify_function.append(pushdeer)
|
||||||
print('未启用 telegram机器人')
|
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
continue
|
notify_function.append(pushplus_bot)
|
||||||
elif i == 'qywxapp_bot':
|
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||||
if QYWX_APP:
|
notify_function.append(qmsg_bot)
|
||||||
qywxapp_bot(title=title, content=content)
|
if push_config.get("QYWX_AM"):
|
||||||
else:
|
notify_function.append(wecom_app)
|
||||||
print('未启用 企业微信应用推送')
|
if push_config.get("QYWX_KEY"):
|
||||||
continue
|
notify_function.append(wecom_bot)
|
||||||
else:
|
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
|
||||||
print('此类推送方式不存在')
|
notify_function.append(telegram_bot)
|
||||||
|
|
||||||
|
|
||||||
|
def send(title: str, content: str) -> None:
|
||||||
|
if not content:
|
||||||
|
print(f"{title} 推送内容为空!")
|
||||||
|
return
|
||||||
|
|
||||||
|
hitokoto = push_config.get("HITOKOTO")
|
||||||
|
|
||||||
|
text = one() if hitokoto else ""
|
||||||
|
content += "\n\n" + text
|
||||||
|
|
||||||
|
ts = [
|
||||||
|
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
|
||||||
|
for mode in notify_function
|
||||||
|
]
|
||||||
|
[t.start() for t in ts]
|
||||||
|
[t.join() for t in ts]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
send('title', 'content')
|
send("title", "content")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
+1
-1
@@ -5,8 +5,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
"download": "^8.0.0",
|
"download": "^8.0.0",
|
||||||
"got": "^11.5.1",
|
|
||||||
"http-server": "^0.12.3",
|
"http-server": "^0.12.3",
|
||||||
|
"got": "^11.5.1",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"tough-cookie": "^4.0.0",
|
"tough-cookie": "^4.0.0",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
requests
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
+61
-46
@@ -1,12 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
get_token() {
|
get_token() {
|
||||||
token=$(cat $file_auth_user | jq -r .token)
|
local tokenFile="$dir_static/build/token.js"
|
||||||
|
if [[ ! -f "$tokenFile" ]]; then
|
||||||
|
token=$(ts-node-transpile-only "$dir_root/back/token.ts")
|
||||||
|
else
|
||||||
|
token=$(node "$tokenFile")
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
add_cron_api() {
|
add_cron_api() {
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
if [ $# -eq 1 ]; then
|
if [[ $# -eq 1 ]]; then
|
||||||
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
||||||
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
||||||
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
||||||
@@ -17,13 +22,13 @@ add_cron_api() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s "http://localhost: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" \
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://localhost:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://localhost:5700/crontab" \
|
-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" \
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
|
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
|
||||||
--compressed
|
--compressed
|
||||||
@@ -39,7 +44,7 @@ add_cron_api() {
|
|||||||
|
|
||||||
update_cron_api() {
|
update_cron_api() {
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
if [ $# -eq 1 ]; then
|
if [[ $# -eq 1 ]]; then
|
||||||
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
||||||
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
||||||
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
||||||
@@ -52,16 +57,16 @@ update_cron_api() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s "http://localhost: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" \
|
||||||
-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" \
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://localhost:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://localhost:5700/crontab" \
|
-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" \
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"_id\":\"$id\"}" \
|
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
code=$(echo $api | jq -r .code)
|
code=$(echo $api | jq -r .code)
|
||||||
@@ -75,7 +80,7 @@ update_cron_api() {
|
|||||||
|
|
||||||
update_cron_command_api() {
|
update_cron_command_api() {
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
if [ $# -eq 1 ]; then
|
if [[ $# -eq 1 ]]; then
|
||||||
local command=$(echo "$1" | awk -F ":" '{print $1}')
|
local command=$(echo "$1" | awk -F ":" '{print $1}')
|
||||||
local id=$(echo "$1" | awk -F ":" '{print $2}')
|
local id=$(echo "$1" | awk -F ":" '{print $2}')
|
||||||
else
|
else
|
||||||
@@ -84,16 +89,16 @@ update_cron_command_api() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s "http://localhost: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" \
|
||||||
-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" \
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://localhost:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://localhost:5700/crontab" \
|
-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" \
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
--data-raw "{\"command\":\"$command\",\"_id\":\"$id\"}" \
|
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
code=$(echo $api | jq -r .code)
|
code=$(echo $api | jq -r .code)
|
||||||
@@ -109,14 +114,14 @@ del_cron_api() {
|
|||||||
local ids=$1
|
local ids=$1
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s "http://localhost: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" \
|
||||||
-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" \
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://localhost:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://localhost:5700/crontab" \
|
-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" \
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
--data-raw "[$ids]" \
|
--data-raw "[$ids]" \
|
||||||
--compressed
|
--compressed
|
||||||
@@ -130,48 +135,58 @@ del_cron_api() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
get_user_info() {
|
update_cron() {
|
||||||
|
local ids="$1"
|
||||||
|
local status="$2"
|
||||||
|
local pid="${3:-''}"
|
||||||
|
local logPath="$4"
|
||||||
|
local lastExecutingTime="${5:-0}"
|
||||||
|
local runningTime="${6:-0}"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s "http://localhost:5600/api/user?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?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://localhost: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_status() {
|
|
||||||
local ids=$1
|
|
||||||
local status=$2
|
|
||||||
local currentTimeStamp=$(date +%s)
|
|
||||||
local api=$(
|
|
||||||
curl -s "http://localhost:5600/api/crons/status?t=$currentTimeStamp" \
|
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-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" \
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://localhost:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://localhost:5700/crontab" \
|
-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" \
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
--data-raw "{\"ids\":[$ids],\"status\":\"$status\"}" \
|
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
|
||||||
|
--compressed
|
||||||
|
)
|
||||||
|
code=$(echo $api | jq -r .code)
|
||||||
|
message=$(echo $api | jq -r .message)
|
||||||
|
if [[ $code != 200 ]]; then
|
||||||
|
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
|
--compressed
|
||||||
)
|
)
|
||||||
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 "更新任务状态成功"
|
echo -e "通知发送成功"
|
||||||
else
|
else
|
||||||
echo -e "更新任务状态失败(${message})"
|
echo -e "通知失败(${message})"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
get_token
|
get_token
|
||||||
|
|||||||
+24
-14
@@ -1,26 +1,30 @@
|
|||||||
#!/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
|
||||||
url="${github_proxy_url}https://github.com/SuMaiKaDe/bot.git"
|
|
||||||
repo_path="${dir_repo}/dockerbot"
|
if [[ -z ${BotRepoUrl} ]]; then
|
||||||
|
url="https://github.com/SuMaiKaDe/bot.git"
|
||||||
|
repo_path="${dir_repo}/dockerbot"
|
||||||
|
else
|
||||||
|
url=${BotRepoUrl}
|
||||||
|
repo_path="${dir_repo}/diybot"
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "\n1、安装bot依赖...\n"
|
echo -e "\n1、安装bot依赖...\n"
|
||||||
apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
|
apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
|
||||||
echo -e "\nbot依赖安装成功...\n"
|
echo -e "\nbot依赖安装成功...\n"
|
||||||
|
|
||||||
echo -e "2、下载bot所需文件...\n"
|
echo -e "2、下载bot所需文件...\n"
|
||||||
if [ -d ${repo_path}/.git ]; then
|
if [[ ! -d ${repo_path}/.git ]]; then
|
||||||
git_pull_scripts ${repo_path}
|
|
||||||
else
|
|
||||||
rm -rf ${repo_path}
|
rm -rf ${repo_path}
|
||||||
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"
|
||||||
|
|
||||||
@@ -28,15 +32,21 @@ 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"
|
||||||
pip3 --default-timeout=100 install -r requirements.txt --no-cache-dir
|
|
||||||
|
cd $dir_data
|
||||||
|
cat requirements.txt | while read LREAD
|
||||||
|
do
|
||||||
|
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
|
||||||
|
pip3 --default-timeout=100 install ${LREAD}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo -e "\npython3依赖安装成功...\n"
|
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"
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
dir_shell=$QL_DIR/shell
|
||||||
|
. $dir_shell/share.sh
|
||||||
|
. $dir_shell/api.sh
|
||||||
|
|
||||||
|
reset_env() {
|
||||||
|
echo -e "---> 1. 开始检测配置文件\n"
|
||||||
|
fix_config
|
||||||
|
echo -e "---> 配置文件检测完成\n"
|
||||||
|
|
||||||
|
echo -e "---> 2. 开始安装青龙依赖\n"
|
||||||
|
npm_install_2 $dir_root
|
||||||
|
echo -e "---> 青龙依赖安装完成\n"
|
||||||
|
|
||||||
|
echo -e "---> 3. 开始安装脚本依赖\n"
|
||||||
|
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||||
|
npm_install_2 $dir_scripts
|
||||||
|
echo -e "---> 脚本依赖安装完成\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_dep() {
|
||||||
|
echo -e "---> 1. 复制通知文件\n"
|
||||||
|
echo -e "---> 复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
||||||
|
cp -fv $file_notify_py_sample $file_notify_py
|
||||||
|
echo
|
||||||
|
echo -e "---> 复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
||||||
|
cp -fv $file_notify_js_sample $file_notify_js
|
||||||
|
echo -e "---> 通知文件复制完成\n"
|
||||||
|
|
||||||
|
echo -e "---> 2. 复制nginx配置文件\n"
|
||||||
|
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||||
|
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||||
|
echo -e "---> 配置文件复制完成\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
pm2_log() {
|
||||||
|
echo -e "---> pm2日志"
|
||||||
|
local panelOut="/root/.pm2/logs/panel-out.log"
|
||||||
|
local panelError="/root/.pm2/logs/panel-error.log"
|
||||||
|
tail -n 100 "$panelOut"
|
||||||
|
tail -n 100 "$panelError"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_nginx() {
|
||||||
|
local nginxPid=$(ps -ef | grep nginx | grep -v grep)
|
||||||
|
echo -e "=====> 检测nginx服务\n$nginxPid"
|
||||||
|
if [[ $nginxPid ]]; then
|
||||||
|
echo -e "\n=====> nginx服务正常\n"
|
||||||
|
nginx -s reload
|
||||||
|
else
|
||||||
|
echo -e "\n=====> nginx服务异常,重新启动nginx\n"
|
||||||
|
nginx -c /etc/nginx/nginx.conf
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_ql() {
|
||||||
|
local api=$(curl -s --noproxy "*" "http://0.0.0.0:5700")
|
||||||
|
echo -e "\n=====> 检测面板\n\n$api\n"
|
||||||
|
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
|
||||||
|
echo -e "=====> 面板服务启动正常\n"
|
||||||
|
else
|
||||||
|
echo -e "=====> 面板服务异常,重置基础环境\n"
|
||||||
|
reset_env
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_pm2() {
|
||||||
|
pm2_log
|
||||||
|
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
|
||||||
|
)
|
||||||
|
echo -e "\n=====> 检测后台\n\n$api\n"
|
||||||
|
if [[ $api =~ "{\"code\"" ]]; then
|
||||||
|
echo -e "=====> 后台服务启动正常\n"
|
||||||
|
else
|
||||||
|
echo -e "=====> 后台服务异常,重置基础环境并重启后台\n"
|
||||||
|
reset_env
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
echo -e "=====> 开始检测"
|
||||||
|
npm i -g pnpm
|
||||||
|
pnpm add -g pm2
|
||||||
|
copy_dep
|
||||||
|
check_ql
|
||||||
|
check_nginx
|
||||||
|
check_pm2
|
||||||
|
reload_pm2
|
||||||
|
echo -e "\n=====> 检测结束\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
-151
@@ -1,151 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
## 导入通用变量与函数
|
|
||||||
dir_shell=/ql/shell
|
|
||||||
. $dir_shell/share.sh
|
|
||||||
|
|
||||||
## 生成pt_pin清单
|
|
||||||
gen_pt_pin_array() {
|
|
||||||
local tmp1 tmp2 i pt_pin_temp
|
|
||||||
for ((user_num = 1; user_num <= $user_sum; user_num++)); do
|
|
||||||
tmp1=Cookie$user_num
|
|
||||||
tmp2=${!tmp1}
|
|
||||||
i=$(($user_num - 1))
|
|
||||||
pt_pin_temp=$(echo $tmp2 | perl -pe "{s|.*pt_pin=([^; ]+)(?=;?).*|\1|; s|%|\\\x|g}")
|
|
||||||
[[ $pt_pin_temp == *\\x* ]] && pt_pin[i]=$(printf $pt_pin_temp) || pt_pin[i]=$pt_pin_temp
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
## 导出互助码的通用程序,$1:去掉后缀的脚本名称,$2:config.sh中的后缀,$3:活动中文名称
|
|
||||||
export_codes_sub() {
|
|
||||||
local task_name=$1
|
|
||||||
local config_name=$2
|
|
||||||
local chinese_name=$3
|
|
||||||
local config_name_my=My$config_name
|
|
||||||
local config_name_for_other=ForOther$config_name
|
|
||||||
local i j k m n pt_pin_in_log code tmp_grep tmp_my_code tmp_for_other user_num random_num_list
|
|
||||||
if cd $dir_log/$task_name &>/dev/null && [[ $(ls) ]]; then
|
|
||||||
## 寻找所有互助码以及对应的pt_pin
|
|
||||||
i=0
|
|
||||||
pt_pin_in_log=()
|
|
||||||
code=()
|
|
||||||
pt_pin_and_code=$(ls -r *.log | xargs awk -v var="的$chinese_name好友互助码" 'BEGIN{FS="[( )】]+"; OFS="&"} $3~var {print $2,$4}')
|
|
||||||
for line in $pt_pin_and_code; do
|
|
||||||
pt_pin_in_log[i]=$(echo $line | awk -F "&" '{print $1}')
|
|
||||||
code[i]=$(echo $line | awk -F "&" '{print $2}')
|
|
||||||
let i++
|
|
||||||
done
|
|
||||||
|
|
||||||
## 输出My系列变量
|
|
||||||
if [[ ${#code[*]} -gt 0 ]]; then
|
|
||||||
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
|
|
||||||
tmp_my_code=""
|
|
||||||
j=$((m + 1))
|
|
||||||
for ((n = 0; n < ${#code[*]}; n++)); do
|
|
||||||
if [[ ${pt_pin[m]} == ${pt_pin_in_log[n]} ]]; then
|
|
||||||
tmp_my_code=${code[n]}
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "$config_name_my$j='$tmp_my_code'"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "## 从日志中未找到任何互助码"
|
|
||||||
fi
|
|
||||||
|
|
||||||
## 输出ForOther系列变量
|
|
||||||
if [[ ${#code[*]} -gt 0 ]]; then
|
|
||||||
echo
|
|
||||||
case $HelpType in
|
|
||||||
0) ## 全部一致
|
|
||||||
tmp_for_other=""
|
|
||||||
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
|
|
||||||
j=$((m + 1))
|
|
||||||
tmp_for_other="$tmp_for_other@\${$config_name_my$j}"
|
|
||||||
done
|
|
||||||
echo "${config_name_for_other}1=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
|
|
||||||
for ((m = 1; m < ${#pt_pin[*]}; m++)); do
|
|
||||||
j=$((m + 1))
|
|
||||||
echo "$config_name_for_other$j=\"\${${config_name_for_other}1}\""
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
|
|
||||||
1) ## 均等助力
|
|
||||||
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
|
|
||||||
tmp_for_other=""
|
|
||||||
j=$((m + 1))
|
|
||||||
for ((n = $m; n < $(($user_sum + $m)); n++)); do
|
|
||||||
[[ $m -eq $n ]] && continue
|
|
||||||
if [[ $((n + 1)) -le $user_sum ]]; then
|
|
||||||
k=$((n + 1))
|
|
||||||
else
|
|
||||||
k=$((n + 1 - $user_sum))
|
|
||||||
fi
|
|
||||||
tmp_for_other="$tmp_for_other@\${$config_name_my$k}"
|
|
||||||
done
|
|
||||||
echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
|
|
||||||
2) ## 本套脚本内账号间随机顺序助力
|
|
||||||
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
|
|
||||||
tmp_for_other=""
|
|
||||||
random_num_list=$(seq $user_sum | sort -R)
|
|
||||||
j=$((m + 1))
|
|
||||||
for n in $random_num_list; do
|
|
||||||
[[ $j -eq $n ]] && continue
|
|
||||||
tmp_for_other="$tmp_for_other@\${$config_name_my$n}"
|
|
||||||
done
|
|
||||||
echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
|
|
||||||
*) ## 按编号优先
|
|
||||||
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
|
|
||||||
tmp_for_other=""
|
|
||||||
j=$((m + 1))
|
|
||||||
for ((n = 0; n < ${#pt_pin[*]}; n++)); do
|
|
||||||
[[ $m -eq $n ]] && continue
|
|
||||||
k=$((n + 1))
|
|
||||||
tmp_for_other="$tmp_for_other@\${$config_name_my$k}"
|
|
||||||
done
|
|
||||||
echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "## 未运行过 $task_name.js 脚本,未产生日志"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
## 汇总输出
|
|
||||||
export_all_codes() {
|
|
||||||
gen_pt_pin_array
|
|
||||||
echo -e "\n# 从日志提取互助码,编号和配置文件中Cookie编号完全对应,如果为空就是所有日志中都没有。\n\n# 即使某个MyXxx变量未赋值,也可以将其变量名填在ForOtherXxx中,jtask脚本会自动过滤空值。\n"
|
|
||||||
echo -n "# 你选择的互助码模板为:"
|
|
||||||
case $HelpType in
|
|
||||||
0)
|
|
||||||
echo "所有账号助力码全部一致。"
|
|
||||||
;;
|
|
||||||
1)
|
|
||||||
echo "所有账号机会均等助力。"
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
echo "本套脚本内账号间随机顺序助力。"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "按账号编号优先。"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
for ((i = 0; i < ${#name_js[*]}; i++)); do
|
|
||||||
echo -e "\n## ${name_chinese[i]}:"
|
|
||||||
export_codes_sub "${name_js[i]}" "${name_config[i]}" "${name_chinese[i]}"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
## 执行并写入日志
|
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
|
||||||
log_path="$dir_code/$log_time.log"
|
|
||||||
make_dir "$dir_code"
|
|
||||||
export_all_codes | perl -pe "{s|京东种豆|种豆|; s|crazyJoy任务|疯狂的JOY|}" | tee $log_path
|
|
||||||
@@ -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,26 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# 导入通用变量与函数
|
|
||||||
dir_shell=/ql/shell
|
|
||||||
. $dir_shell/share.sh
|
|
||||||
|
|
||||||
echo -e "1. 开始安装青龙依赖\n"
|
|
||||||
npm_install_2 $dir_root
|
|
||||||
echo -e "青龙依赖安装完成\n"
|
|
||||||
|
|
||||||
echo -e "2. 开始安装脚本依赖\n"
|
|
||||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
|
||||||
npm_install_2 $dir_scripts
|
|
||||||
echo -e "脚本依赖安装完成\n"
|
|
||||||
|
|
||||||
echo -e "3. 复制通知文件\n"
|
|
||||||
echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
|
||||||
cp -fv $file_notify_py_sample $file_notify_py
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo -e "复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
|
||||||
cp -fv $file_notify_js_sample $file_notify_js
|
|
||||||
|
|
||||||
echo -e "通知文件复制完成\n"
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
+2
-2
@@ -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
|
||||||
@@ -27,7 +27,7 @@ remove_js_log() {
|
|||||||
remove_empty_dir() {
|
remove_empty_dir() {
|
||||||
cd $dir_log
|
cd $dir_log
|
||||||
for dir in $(ls); do
|
for dir in $(ls); do
|
||||||
if [ -d $dir ] && [[ -z $(ls $dir) ]]; then
|
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
|
||||||
rm -rf $dir
|
rm -rf $dir
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
+211
-80
@@ -1,34 +1,43 @@
|
|||||||
#!/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_manual_log=$dir_root/manual_log
|
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
|
||||||
|
|
||||||
## 文件
|
## 文件
|
||||||
file_config_sample=$dir_sample/config.sample.sh
|
file_config_sample=$dir_sample/config.sample.sh
|
||||||
file_cookie=$dir_config/cookie.sh
|
file_env=$dir_config/env.sh
|
||||||
file_sharecode=$dir_config/sharecode.sh
|
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_extra_shell=$dir_config/extra.sh
|
file_extra_shell=$dir_config/extra.sh
|
||||||
|
file_task_before=$dir_config/task_before.sh
|
||||||
|
file_task_after=$dir_config/task_after.sh
|
||||||
|
file_task_sample=$dir_sample/task.sample.sh
|
||||||
file_extra_sample=$dir_sample/extra.sample.sh
|
file_extra_sample=$dir_sample/extra.sample.sh
|
||||||
file_notify_js_sample=$dir_sample/notify.js
|
file_notify_js_sample=$dir_sample/notify.js
|
||||||
file_notify_py_sample=$dir_sample/notify.py
|
file_notify_py_sample=$dir_sample/notify.py
|
||||||
file_notify_py=$dir_scripts/notify.py
|
file_notify_py=$dir_scripts/notify.py
|
||||||
file_notify_js=$dir_scripts/sendNotify.js
|
file_notify_js=$dir_scripts/sendNotify.js
|
||||||
|
task_error_log_path=$dir_log/task_error.log
|
||||||
|
nginx_app_conf=$dir_root/docker/front.conf
|
||||||
|
nginx_conf=$dir_root/docker/nginx.conf
|
||||||
|
dep_notify_py=$dir_dep/notify.py
|
||||||
|
dep_notify_js=$dir_dep/sendNotify.js
|
||||||
|
|
||||||
## 清单文件
|
## 清单文件
|
||||||
list_crontab_user=$dir_config/crontab.list
|
list_crontab_user=$dir_config/crontab.list
|
||||||
@@ -42,30 +51,45 @@ 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() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
import_config() {
|
import_config() {
|
||||||
[ -f $file_config_user ] && . $file_config_user
|
[[ -f $file_config_user ]] && . $file_config_user
|
||||||
user_sum=0
|
[[ -f $file_env ]] && . $file_env
|
||||||
for line in $(cat $file_cookie); do
|
|
||||||
let user_sum+=1
|
|
||||||
eval Cookie${user_sum}="\"${line}\""
|
|
||||||
done
|
|
||||||
|
|
||||||
command_timeout_time=${CommandTimeoutTime:-"1h"}
|
command_timeout_time=${CommandTimeoutTime:-"1h"}
|
||||||
github_proxy_url=${GithubProxyUrl:-""}
|
proxy_url=${ProxyUrl:-""}
|
||||||
block_cookie=${TempBlockCookie:-""}
|
|
||||||
file_extensions=${RepoFileExtensions:-"js py"}
|
file_extensions=${RepoFileExtensions:-"js py"}
|
||||||
|
current_branch=${QL_BRANCH}
|
||||||
|
|
||||||
|
if [[ -n "${DefaultCronRule}" ]]; then
|
||||||
|
default_cron="${DefaultCronRule}"
|
||||||
|
else
|
||||||
|
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
set_proxy() {
|
||||||
|
if [[ $proxy_url ]]; then
|
||||||
|
export http_proxy="${proxy_url}"
|
||||||
|
export https_proxy="${proxy_url}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
unset_proxy() {
|
||||||
|
unset http_proxy
|
||||||
|
unset https_proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
## 创建目录,$1:目录的绝对路径
|
|
||||||
make_dir() {
|
make_dir() {
|
||||||
local dir=$1
|
local dir=$1
|
||||||
if [[ ! -d $dir ]]; then
|
if [[ ! -d $dir ]]; then
|
||||||
@@ -73,41 +97,36 @@ make_dir() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
## 检测termux
|
|
||||||
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
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
## 检测macos
|
|
||||||
detect_macos() {
|
detect_macos() {
|
||||||
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
|
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
|
||||||
}
|
}
|
||||||
|
|
||||||
## 生成随机数,$1:用来求余的数字
|
|
||||||
gen_random_num() {
|
gen_random_num() {
|
||||||
local divi=$1
|
local divi=$1
|
||||||
echo $((${RANDOM} % $divi))
|
echo $((${RANDOM} % $divi))
|
||||||
}
|
}
|
||||||
|
|
||||||
## 创建软连接的子函数,$1:软连接文件路径,$2:要连接的对象
|
|
||||||
link_shell_sub() {
|
link_shell_sub() {
|
||||||
local link_path="$1"
|
local link_path="$1"
|
||||||
local original_path="$2"
|
local original_path="$2"
|
||||||
if [ ! -L $link_path ] || [[ $(readlink -f $link_path) != $original_path ]]; then
|
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
|
||||||
rm -f $link_path 2>/dev/null
|
rm -f $link_path 2>/dev/null
|
||||||
ln -sf $original_path $link_path
|
ln -sf $original_path $link_path
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
## 创建软连接
|
|
||||||
link_shell() {
|
link_shell() {
|
||||||
if [[ $is_termux -eq 1 ]]; then
|
if [[ $is_termux -eq 1 ]]; then
|
||||||
local path="/data/data/com.termux/files/usr/bin/"
|
local path="/data/data/com.termux/files/usr/bin/"
|
||||||
elif [[ $PATH == */usr/local/bin* ]] && [ -d /usr/local/bin ]; then
|
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
|
||||||
local path="/usr/local/bin/"
|
local path="/usr/local/bin/"
|
||||||
else
|
else
|
||||||
local path=""
|
local path=""
|
||||||
@@ -120,19 +139,18 @@ link_shell() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
## 定义各命令
|
|
||||||
define_cmd() {
|
define_cmd() {
|
||||||
local cmd_prefix cmd_suffix
|
local cmd_prefix cmd_suffix
|
||||||
if type task >/dev/null 2>&1; then
|
if type task &>/dev/null; then
|
||||||
cmd_suffix=""
|
cmd_suffix=""
|
||||||
if [ -x "$dir_shell/task.sh" ]; then
|
if [[ -f "$dir_shell/task.sh" ]]; then
|
||||||
cmd_prefix=""
|
cmd_prefix=""
|
||||||
else
|
else
|
||||||
cmd_prefix="bash "
|
cmd_prefix="bash "
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
cmd_suffix=".sh"
|
cmd_suffix=".sh"
|
||||||
if [ -x "$dir_shell/task.sh" ]; then
|
if [[ -f "$dir_shell/task.sh" ]]; then
|
||||||
cmd_prefix="$dir_shell/"
|
cmd_prefix="$dir_shell/"
|
||||||
else
|
else
|
||||||
cmd_prefix="bash $dir_shell/"
|
cmd_prefix="bash $dir_shell/"
|
||||||
@@ -143,74 +161,99 @@ define_cmd() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
## 修复配置文件
|
|
||||||
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
|
||||||
make_dir $dir_manual_log
|
|
||||||
make_dir $dir_scripts
|
make_dir $dir_scripts
|
||||||
make_dir $dir_list_tmp
|
make_dir $dir_list_tmp
|
||||||
make_dir $dir_repo
|
make_dir $dir_repo
|
||||||
make_dir $dir_raw
|
make_dir $dir_raw
|
||||||
make_dir $dir_update_log
|
make_dir $dir_update_log
|
||||||
|
make_dir $dir_dep
|
||||||
|
|
||||||
if [ ! -s $file_config_user ]; then
|
if [[ ! -s $file_config_user ]]; then
|
||||||
echo -e "复制一份 $file_config_sample 为 $file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
|
echo -e "复制一份 $file_config_sample 为 $file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
|
||||||
cp -fv $file_config_sample $file_config_user
|
cp -fv $file_config_sample $file_config_user
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f $file_cookie ]; then
|
if [[ ! -f $file_env ]]; then
|
||||||
echo -e "检测到config配置目录下不存在cookie.sh,创建一个空文件用于初始化...\n"
|
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
|
||||||
touch $file_cookie
|
touch $file_env
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f $file_extra_shell ]; then
|
if [[ ! -f $file_task_before ]]; then
|
||||||
|
echo -e "复制一份 $file_task_sample 为 $file_task_before\n"
|
||||||
|
cp -fv $file_task_sample $file_task_before
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f $file_task_after ]]; then
|
||||||
|
echo -e "复制一份 $file_task_sample 为 $file_task_after\n"
|
||||||
|
cp -fv $file_task_sample $file_task_after
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f $file_extra_shell ]]; then
|
||||||
echo -e "复制一份 $file_extra_sample 为 $file_extra_shell\n"
|
echo -e "复制一份 $file_extra_sample 为 $file_extra_shell\n"
|
||||||
cp -fv $file_extra_sample $file_extra_shell
|
cp -fv $file_extra_sample $file_extra_shell
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s $file_auth_user ]; then
|
if [[ ! -s $file_auth_user ]]; then
|
||||||
echo -e "复制一份 $file_auth_sample 为 $file_auth_user\n"
|
echo -e "复制一份 $file_auth_sample 为 $file_auth_user\n"
|
||||||
cp -fv $file_auth_sample $file_auth_user
|
cp -fv $file_auth_sample $file_auth_user
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s $file_notify_py ]; then
|
if [[ ! -s $file_notify_py ]]; then
|
||||||
echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
||||||
cp -fv $file_notify_py_sample $file_notify_py
|
cp -fv $file_notify_py_sample $file_notify_py
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s $file_notify_js ]; then
|
if [[ ! -s $file_notify_js ]]; then
|
||||||
echo -e "复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
echo -e "复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
||||||
cp -fv $file_notify_js_sample $file_notify_js
|
cp -fv $file_notify_js_sample $file_notify_js
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -s /etc/nginx/conf.d/default.conf ]; then
|
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
|
||||||
echo -e "检测到默认nginx配置文件,删除...\n"
|
echo -e "检测到默认nginx配置文件,清空...\n"
|
||||||
rm -f /etc/nginx/conf.d/default.conf
|
cat /dev/null >/etc/nginx/conf.d/default.conf
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
## npm install 子程序,判断是否为安卓,判断是否安装有pnpm
|
if [[ ! -s $dep_notify_js ]]; then
|
||||||
npm_install_sub() {
|
echo -e "复制一份 $file_notify_js_sample 为 $dep_notify_js\n"
|
||||||
if [ $is_termux -eq 1 ]; then
|
cp -fv $file_notify_js_sample $dep_notify_js
|
||||||
npm install --production --no-save --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links --no-save
|
echo
|
||||||
elif ! type pnpm >/dev/null 2>&1; then
|
|
||||||
npm install --production --no-save --registry=https://registry.npm.taobao.org || npm install --production --no-save
|
|
||||||
else
|
|
||||||
echo -e "检测到本机安装了 pnpm,使用 pnpm 替代 ...\n"
|
|
||||||
pnpm install --prod
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s $dep_notify_py ]]; then
|
||||||
|
echo -e "复制一份 $file_notify_py_sample 为 $dep_notify_py\n"
|
||||||
|
cp -fv $file_notify_py_sample $dep_notify_py
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
npm_install_sub() {
|
||||||
|
set_proxy
|
||||||
|
if [ $is_termux -eq 1 ]; then
|
||||||
|
npm install --production --no-bin-links --registry=https://registry.npmmirror.com || npm install --production --no-bin-links
|
||||||
|
elif ! type pnpm &>/dev/null; then
|
||||||
|
npm install --production --registry=https://registry.npmmirror.com || npm install --production
|
||||||
|
else
|
||||||
|
pnpm install --loglevel error --production --registry=https://registry.npmmirror.com || pnpm install --production --loglevel error
|
||||||
|
fi
|
||||||
|
unset_proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
## npm install,$1:package.json文件所在路径
|
|
||||||
npm_install_1() {
|
npm_install_1() {
|
||||||
local dir_current=$(pwd)
|
local dir_current=$(pwd)
|
||||||
local dir_work=$1
|
local dir_work=$1
|
||||||
@@ -236,66 +279,154 @@ npm_install_2() {
|
|||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
## 比对两个文件,$1比$2新时,将$1复制为$2
|
|
||||||
diff_and_copy() {
|
diff_and_copy() {
|
||||||
local copy_source=$1
|
local copy_source=$1
|
||||||
local copy_to=$2
|
local copy_to=$2
|
||||||
if [ ! -s $copy_to ] || [[ $(diff $copy_source $copy_to) ]]; then
|
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
|
||||||
cp -f $copy_source $copy_to
|
cp -f $copy_source $copy_to
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新依赖
|
|
||||||
update_depend() {
|
update_depend() {
|
||||||
local dir_current=$(pwd)
|
local dir_current=$(pwd)
|
||||||
|
|
||||||
if [ ! -s $dir_scripts/package.json ] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
|
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
|
||||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||||
npm_install_2 $dir_scripts
|
npm_install_2 $dir_scripts
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s $dir_scripts/requirements.txt ] || [[ $(diff $dir_sample/requirements.txt $dir_scripts/requirements.txt) ]]; then
|
|
||||||
cp -f $dir_sample/requirements.txt $dir_scripts/requirements.txt
|
|
||||||
cd $dir_scripts
|
|
||||||
pip3 install -r $dir_scripts/requirements.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
## 克隆脚本,$1:仓库地址,$2:仓库保存路径,$3:分支(可省略)
|
|
||||||
git_clone_scripts() {
|
git_clone_scripts() {
|
||||||
local url=$1
|
local url=$1
|
||||||
local dir=$2
|
local dir=$2
|
||||||
local branch=$3
|
local branch=$3
|
||||||
[[ $branch ]] && local cmd="-b $branch "
|
[[ $branch ]] && local part_cmd="-b $branch "
|
||||||
echo -e "开始克隆仓库 $url 到 $dir\n"
|
echo -e "开始克隆仓库 $url 到 $dir\n"
|
||||||
git clone $cmd $url $dir
|
|
||||||
|
set_proxy
|
||||||
|
git clone $part_cmd $url $dir
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
|
unset_proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新脚本,$1:仓库保存路径
|
|
||||||
git_pull_scripts() {
|
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 cmd="origin/${branch}"
|
|
||||||
cd $dir_work
|
cd $dir_work
|
||||||
echo -e "开始更新仓库:$dir_work\n"
|
echo -e "开始更新仓库:$dir_work\n"
|
||||||
|
|
||||||
|
set_proxy
|
||||||
git fetch --all
|
git fetch --all
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
git reset --hard $cmd
|
git pull &>/dev/null
|
||||||
git pull
|
unset_proxy
|
||||||
|
reset_branch "$branch"
|
||||||
|
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
init_env() {
|
reset_romote_url() {
|
||||||
TempBlockCookie=""
|
local dir_current=$(pwd)
|
||||||
|
local dir_work=$1
|
||||||
|
local url=$2
|
||||||
|
local branch="$3"
|
||||||
|
|
||||||
|
if [[ -d "$dir_work/.git" ]]; then
|
||||||
|
cd $dir_work
|
||||||
|
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
||||||
|
git remote set-url origin $url &>/dev/null
|
||||||
|
|
||||||
|
local part_cmd=""
|
||||||
|
reset_branch "$branch"
|
||||||
|
cd $dir_current
|
||||||
|
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() {
|
||||||
|
local beg=$1
|
||||||
|
local end=$2
|
||||||
|
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
|
||||||
detect_termux
|
detect_termux
|
||||||
detect_macos
|
detect_macos
|
||||||
define_cmd
|
define_cmd
|
||||||
init_env
|
fix_config
|
||||||
import_config $1
|
|
||||||
|
import_config $1 >$task_error_log_path 2>&1
|
||||||
|
|||||||
+299
-98
@@ -1,52 +1,21 @@
|
|||||||
#!/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
|
||||||
|
|
||||||
## 组合Cookie和互助码子程序,$1:要组合的内容
|
|
||||||
combine_sub() {
|
|
||||||
local what_combine=$1
|
|
||||||
local combined_all=""
|
|
||||||
local tmp1 tmp2
|
|
||||||
for ((i = 1; i <= $user_sum; i++)); do
|
|
||||||
for num in $block_cookie; do
|
|
||||||
[[ $i -eq $num ]] && continue 2
|
|
||||||
done
|
|
||||||
local tmp1=$what_combine$i
|
|
||||||
local tmp2=${!tmp1}
|
|
||||||
combined_all="$combined_all&$tmp2"
|
|
||||||
done
|
|
||||||
echo $combined_all | perl -pe "{s|^&||; s|^@+||; s|&@|&|g; s|@+&|&|g; s|@+|@|g; s|@+$||}"
|
|
||||||
}
|
|
||||||
|
|
||||||
## 正常依次运行时,组合所有账号的Cookie与互助码
|
|
||||||
combine_all() {
|
|
||||||
for ((i = 0; i < ${#env_name[*]}; i++)); do
|
|
||||||
result=$(combine_sub ${var_name[i]})
|
|
||||||
if [[ $result ]]; then
|
|
||||||
export ${env_name[i]}="$result"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
## 并发运行时,直接申明每个账号的Cookie与互助码,$1:用户Cookie编号
|
|
||||||
combine_one() {
|
|
||||||
local user_num=$1
|
|
||||||
for ((i = 0; i < ${#env_name[*]}; i++)); do
|
|
||||||
local tmp=${var_name[i]}$user_num
|
|
||||||
export ${env_name[i]}=${!tmp}
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
## 选择python3还是node
|
## 选择python3还是node
|
||||||
define_program() {
|
define_program() {
|
||||||
local p1=$1
|
local file_param=$1
|
||||||
if [[ $p1 == *.js ]]; then
|
if [[ $file_param == *.js ]]; then
|
||||||
which_program="node"
|
which_program="node"
|
||||||
elif [[ $p1 == *.py ]]; then
|
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
|
||||||
which_program="python3"
|
which_program="python3"
|
||||||
|
elif [[ $file_param == *.sh ]]; then
|
||||||
|
which_program="bash"
|
||||||
|
elif [[ $file_param == *.ts ]]; then
|
||||||
|
which_program="ts-node-transpile-only"
|
||||||
else
|
else
|
||||||
which_program=""
|
which_program=""
|
||||||
fi
|
fi
|
||||||
@@ -70,7 +39,7 @@ gen_array_scripts() {
|
|||||||
local i="-1"
|
local i="-1"
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
for file in $(ls); do
|
for file in $(ls); do
|
||||||
if [ -f $file ] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
||||||
let i++
|
let i++
|
||||||
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
|
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
|
||||||
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
|
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
|
||||||
@@ -85,9 +54,10 @@ usage() {
|
|||||||
define_cmd
|
define_cmd
|
||||||
gen_array_scripts
|
gen_array_scripts
|
||||||
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
|
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
|
||||||
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
|
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
|
||||||
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
|
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
|
||||||
echo -e "3.$cmd_task <file_name> conc # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中"
|
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
|
||||||
|
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
|
||||||
if [[ ${#array_scripts[*]} -gt 0 ]]; then
|
if [[ ${#array_scripts[*]} -gt 0 ]]; then
|
||||||
echo -e "\n当前有以下脚本可以运行:"
|
echo -e "\n当前有以下脚本可以运行:"
|
||||||
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
|
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
|
||||||
@@ -106,84 +76,315 @@ run_nohup() {
|
|||||||
|
|
||||||
## 正常运行单个脚本,$1:传入参数
|
## 正常运行单个脚本,$1:传入参数
|
||||||
run_normal() {
|
run_normal() {
|
||||||
local p1=$1
|
local file_param=$1
|
||||||
cd $dir_scripts
|
define_program "$file_param"
|
||||||
define_program "$p1"
|
if [[ $file_param == *.js ]]; then
|
||||||
if [[ $p1 == *.js ]]; then
|
|
||||||
if [[ $AutoHelpOther == true ]] && [[ $(ls $dir_code) ]]; then
|
|
||||||
local latest_log=$(ls -r $dir_code | head -1)
|
|
||||||
. $dir_code/$latest_log
|
|
||||||
fi
|
|
||||||
if [[ $# -eq 1 ]]; then
|
if [[ $# -eq 1 ]]; then
|
||||||
random_delay
|
random_delay
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
combine_all
|
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
|
||||||
log_dir_tmp="${p1##*/}"
|
|
||||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
|
||||||
log_path="$log_dir/$log_time.log"
|
|
||||||
make_dir "$log_dir"
|
|
||||||
|
|
||||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $p1$" | perl -pe "s|.*ID=(.*) $cmd_task $p1$|\1|" | xargs | sed 's/ /","/g')
|
local time=$(date "+$time_format")
|
||||||
update_cron_status "\"$id\"" "0"
|
log_time=$(format_log_time "$time_format" "$time")
|
||||||
timeout $command_timeout_time $which_program $p1 2>&1 | tee $log_path
|
log_dir_tmp="${file_param##*/}"
|
||||||
update_cron_status "\"$id\"" "1"
|
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="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
|
log_dir="${log_dir_tmp%.*}"
|
||||||
|
log_path="$log_dir/$log_time.log"
|
||||||
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
|
make_dir "$dir_log/$log_dir"
|
||||||
|
|
||||||
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
|
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
|
||||||
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
|
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"
|
||||||
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
|
cd $dir_scripts
|
||||||
|
local relative_path="${file_param%/*}"
|
||||||
|
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||||
|
cd ${relative_path}
|
||||||
|
file_param=${file_param/$relative_path\//}
|
||||||
|
fi
|
||||||
|
|
||||||
|
eval $timeoutCmd $which_program $file_param $cmd
|
||||||
|
|
||||||
|
eval . $file_task_after "$@" $cmd
|
||||||
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
local end_timestamp=$(date "+%s")
|
||||||
|
local diff_time=$(expr $end_timestamp - $begin_timestamp)
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
## 并发执行,因为是并发,所以日志只能直接记录在日志文件中(日志文件以Cookie编号结尾),前台执行并发跑时不会输出日志
|
|
||||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||||
run_concurrent() {
|
run_concurrent() {
|
||||||
local p1=$1
|
local file_param="$1"
|
||||||
cd $dir_scripts
|
local env_param="$2"
|
||||||
define_program "$p1"
|
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
||||||
log_dir="$dir_log/${p1%%.*}"
|
if [[ ! $env_param ]]; then
|
||||||
make_dir $log_dir
|
echo -e "\n 缺少并发运行的环境变量参数"
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
exit 1
|
||||||
echo -e "\n各账号间已经在后台开始并发执行,前台不输入日志,日志直接写入文件中。\n"
|
fi
|
||||||
for ((user_num = 1; user_num <= $user_sum; user_num++)); do
|
|
||||||
combine_one $user_num
|
local envs=$(eval echo "\$${env_param}")
|
||||||
log_path="$log_dir/${log_time}_${user_num}.log"
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
timeout $command_timeout_time $which_program $p1 &>$log_path &
|
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||||
|
local runArr=($(eval echo $tempArr))
|
||||||
|
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
|
||||||
|
|
||||||
|
local n=0
|
||||||
|
for i in ${runArr[@]}; do
|
||||||
|
array_run[n]=${array[$i - 1]}
|
||||||
|
let n++
|
||||||
done
|
done
|
||||||
|
|
||||||
|
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||||
|
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||||
|
|
||||||
|
define_program "$file_param"
|
||||||
|
local time=$(date "+$time_format")
|
||||||
|
log_time=$(format_log_time "$time_format" "$time")
|
||||||
|
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="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
|
log_dir="${log_dir_tmp%.*}"
|
||||||
|
log_path="$log_dir/$log_time.log"
|
||||||
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
|
make_dir "$dir_log/$log_dir"
|
||||||
|
|
||||||
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
|
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
|
||||||
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
|
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"
|
||||||
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
|
local envs=$(eval echo "\$${env_param}")
|
||||||
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
|
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
||||||
|
|
||||||
|
cd $dir_scripts
|
||||||
|
local relative_path="${file_param%/*}"
|
||||||
|
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||||
|
cd ${relative_path}
|
||||||
|
file_param=${file_param/$relative_path\//}
|
||||||
|
fi
|
||||||
|
for i in "${!array[@]}"; do
|
||||||
|
export ${env_param}=${array[i]}
|
||||||
|
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||||
|
eval $timeoutCmd $which_program $file_param &>$single_log_path &
|
||||||
|
done
|
||||||
|
|
||||||
|
wait
|
||||||
|
for i in "${!array[@]}"; do
|
||||||
|
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||||
|
eval cat $single_log_path $cmd
|
||||||
|
[[ -f $single_log_path ]] && rm -f $single_log_path
|
||||||
|
done
|
||||||
|
|
||||||
|
eval . $file_task_after "$@" $cmd
|
||||||
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
local end_timestamp=$(date "+%s")
|
||||||
|
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
run_designated() {
|
||||||
|
local file_param="$1"
|
||||||
|
local env_param="$2"
|
||||||
|
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
||||||
|
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
|
||||||
|
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
define_program "$file_param"
|
||||||
|
local time=$(date "+$time_format")
|
||||||
|
log_time=$(format_log_time "$time_format" "$time")
|
||||||
|
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="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
|
log_dir="${log_dir_tmp%.*}"
|
||||||
|
log_path="$log_dir/$log_time.log"
|
||||||
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
|
make_dir "$dir_log/$log_dir"
|
||||||
|
|
||||||
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
|
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
|
||||||
|
local envs=$(eval echo "\$${env_param}")
|
||||||
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
|
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||||
|
local runArr=($(eval echo $tempArr))
|
||||||
|
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
|
||||||
|
|
||||||
|
local n=0
|
||||||
|
for i in ${runArr[@]}; do
|
||||||
|
array_run[n]=${array[$i - 1]}
|
||||||
|
let n++
|
||||||
|
done
|
||||||
|
|
||||||
|
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||||
|
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||||
|
|
||||||
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
|
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"
|
||||||
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
|
cd $dir_scripts
|
||||||
|
local relative_path="${file_param%/*}"
|
||||||
|
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||||
|
cd ${relative_path}
|
||||||
|
file_param=${file_param/$relative_path\//}
|
||||||
|
fi
|
||||||
|
eval $timeoutCmd $which_program $file_param $cmd
|
||||||
|
|
||||||
|
eval . $file_task_after "$@" $cmd
|
||||||
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
local end_timestamp=$(date "+%s")
|
||||||
|
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
## 运行其他命令
|
## 运行其他命令
|
||||||
run_else() {
|
run_else() {
|
||||||
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
local file_param="$1"
|
||||||
local log_dir="$dir_log/$1"
|
define_program "$file_param"
|
||||||
local log_path="$log_dir/$log_time.log"
|
local time=$(date "+$time_format")
|
||||||
make_dir "$log_dir"
|
log_time=$(format_log_time "$time_format" "$time")
|
||||||
timeout $command_timeout_time "$@" 2>&1 | tee $log_path
|
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="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
|
log_dir="${log_dir_tmp%.*}"
|
||||||
|
log_path="$log_dir/$log_time.log"
|
||||||
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
|
make_dir "$dir_log/$log_dir"
|
||||||
|
|
||||||
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
|
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
|
||||||
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
|
local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
|
cd $dir_scripts
|
||||||
|
local relative_path="${file_param%/*}"
|
||||||
|
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||||
|
cd ${relative_path}
|
||||||
|
file_param=${file_param/$relative_path\//}
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
eval $timeoutCmd $which_program "$file_param" "$@" $cmd
|
||||||
|
|
||||||
|
eval . $file_task_after "$file_param" "$@" $cmd
|
||||||
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
local end_timestamp=$(date "+%s")
|
||||||
|
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
## 命令检测
|
## 命令检测
|
||||||
main() {
|
main() {
|
||||||
case $# in
|
show_log="false"
|
||||||
0)
|
while getopts ":l" opt
|
||||||
echo
|
do
|
||||||
usage
|
case $opt in
|
||||||
;;
|
l)
|
||||||
1)
|
show_log="true"
|
||||||
run_normal $1
|
;;
|
||||||
;;
|
esac
|
||||||
2)
|
done
|
||||||
case $2 in
|
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
||||||
now)
|
|
||||||
run_normal $1 $2
|
timeoutCmd=""
|
||||||
;;
|
if type timeout &>/dev/null; then
|
||||||
conc)
|
timeoutCmd="timeout -k 10s $command_timeout_time "
|
||||||
run_concurrent $1 $2
|
fi
|
||||||
|
|
||||||
|
time_format="%Y-%m-%d %H:%M:%S"
|
||||||
|
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
|
||||||
|
case $# in
|
||||||
|
1)
|
||||||
|
run_normal "$1"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
run_else "$@"
|
case $2 in
|
||||||
|
now)
|
||||||
|
run_normal "$1" "$2"
|
||||||
|
;;
|
||||||
|
conc)
|
||||||
|
run_concurrent "$1" "$3" "$*"
|
||||||
|
;;
|
||||||
|
desi)
|
||||||
|
run_designated "$1" "$3" "$*"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
run_else "$@"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
|
||||||
*)
|
elif [[ $# -eq 0 ]]; then
|
||||||
|
echo
|
||||||
|
usage
|
||||||
|
else
|
||||||
run_else "$@"
|
run_else "$@"
|
||||||
;;
|
fi
|
||||||
esac
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
+238
-141
@@ -1,43 +1,27 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# 导入通用变量与函数
|
dir_shell=$QL_DIR/shell
|
||||||
dir_shell=/ql/shell
|
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/api.sh
|
. $dir_shell/api.sh
|
||||||
|
|
||||||
send_mark=$dir_shell/send_mark
|
send_mark=$dir_shell/send_mark
|
||||||
|
|
||||||
## 重置仓库remote url,docker专用,$1:要重置的目录,$2:要重置为的网址
|
|
||||||
reset_romote_url() {
|
|
||||||
local dir_current=$(pwd)
|
|
||||||
local dir_work=$1
|
|
||||||
local url=$2
|
|
||||||
local branch="$3"
|
|
||||||
|
|
||||||
[[ $branch ]] && local cmd="origin/${branch}"
|
|
||||||
|
|
||||||
if [ -d "$dir_work/.git" ]; then
|
|
||||||
cd $dir_work
|
|
||||||
git remote set-url origin $url >/dev/null
|
|
||||||
git reset --hard $cmd >/dev/null
|
|
||||||
cd $dir_current
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
|
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
|
||||||
diff_cron() {
|
diff_cron() {
|
||||||
local list_scripts="$1"
|
local list_scripts="$1"
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -46,26 +30,26 @@ diff_cron() {
|
|||||||
detect_config_version() {
|
detect_config_version() {
|
||||||
## 识别出两个文件的版本号
|
## 识别出两个文件的版本号
|
||||||
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
||||||
[ -f $file_config_user ] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
||||||
|
|
||||||
## 删除旧的发送记录文件
|
## 删除旧的发送记录文件
|
||||||
[ -f $send_mark ] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
|
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
|
||||||
|
|
||||||
## 识别出更新日期和更新内容
|
## 识别出更新日期和更新内容
|
||||||
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
|
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
|
||||||
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
|
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
|
||||||
|
|
||||||
## 如果是今天,并且版本号不一致,则发送通知
|
## 如果是今天,并且版本号不一致,则发送通知
|
||||||
if [ -f $file_config_user ] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
|
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
|
||||||
if [ ! -f $send_mark ]; then
|
if [[ ! -f $send_mark ]]; then
|
||||||
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
|
||||||
[ -f $send_mark ] && rm -f $send_mark
|
[[ -f $send_mark ]] && rm -f $send_mark
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +57,7 @@ detect_config_version() {
|
|||||||
output_list_add_drop() {
|
output_list_add_drop() {
|
||||||
local list=$1
|
local list=$1
|
||||||
local type=$2
|
local type=$2
|
||||||
if [ -s $list ]; then
|
if [[ -s $list ]]; then
|
||||||
echo -e "检测到有$type的定时任务:\n"
|
echo -e "检测到有$type的定时任务:\n"
|
||||||
cat $list
|
cat $list
|
||||||
echo
|
echo
|
||||||
@@ -89,7 +73,7 @@ del_cron() {
|
|||||||
local ids=""
|
local ids=""
|
||||||
echo -e "开始尝试自动删除失效的定时任务...\n"
|
echo -e "开始尝试自动删除失效的定时任务...\n"
|
||||||
for cron in $(cat $list_drop); do
|
for cron in $(cat $list_drop); do
|
||||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron|\1|" | xargs | sed 's/ /","/g' | head -1)
|
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
|
||||||
if [[ $ids ]]; then
|
if [[ $ids ]]; then
|
||||||
ids="$ids,\"$id\""
|
ids="$ids,\"$id\""
|
||||||
else
|
else
|
||||||
@@ -109,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +106,9 @@ add_cron() {
|
|||||||
local detail=""
|
local detail=""
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
for file in $(cat $list_add); do
|
for file in $(cat $list_add); do
|
||||||
local file_name=${file/${path}\_/}
|
local file_name=${file/${path}\//}
|
||||||
if [ -f $file ]; then
|
file_name=${file_name/${path}\_/}
|
||||||
|
if [[ -f $file ]]; then
|
||||||
cron_line=$(
|
cron_line=$(
|
||||||
perl -ne "{
|
perl -ne "{
|
||||||
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
|
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
|
||||||
@@ -136,66 +121,72 @@ add_cron() {
|
|||||||
)
|
)
|
||||||
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
||||||
[[ -z $cron_name ]] && cron_name="$file_name"
|
[[ -z $cron_name ]] && cron_name="$file_name"
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line="0 6 * * *"
|
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||||
|
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||||
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
|
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
|
||||||
echo -e "$result"
|
echo -e "$result"
|
||||||
if [[ $detail ]]; then
|
if [[ $detail ]]; then
|
||||||
detail="${detail}\n${result}"
|
detail="${detail}${result}\n"
|
||||||
else
|
else
|
||||||
detail="${result}"
|
detail="${result}\n"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
notify "$path 新增任务" "$detail"
|
notify_api "$path 新增任务" "$detail"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新仓库
|
## 更新仓库
|
||||||
update_repo() {
|
update_repo() {
|
||||||
echo -e "--------------------------------------------------------------\n"
|
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local path="$2"
|
local path="$2"
|
||||||
local blackword="$3"
|
local blackword="$3"
|
||||||
local dependence="$4"
|
local dependence="$4"
|
||||||
local branch="$5"
|
local branch="$5"
|
||||||
local urlTmp="${url%*/}"
|
local extensions="$6"
|
||||||
local repoTmp="${urlTmp##*/}"
|
|
||||||
local repo="${repoTmp%.*}"
|
|
||||||
local tmp="${url%/*}"
|
local tmp="${url%/*}"
|
||||||
local authorTmp1="${tmp##*/}"
|
local authorTmp1="${tmp##*/}"
|
||||||
local authorTmp2="${authorTmp1##*:}"
|
local authorTmp2="${authorTmp1##*:}"
|
||||||
local author="${authorTmp2##*.}"
|
local author="${authorTmp2##*.}"
|
||||||
|
|
||||||
local repo_path="${dir_repo}/${author}_${repo}"
|
local repo_path="${dir_repo}/${uniq_path}"
|
||||||
[[ $branch ]] && repo_path="${repo_path}_${branch}"
|
|
||||||
|
|
||||||
if [ -d ${repo_path}/.git ]; then
|
make_dir "${dir_scripts}/${uniq_path}"
|
||||||
reset_romote_url ${repo_path} "${github_proxy_url}${url/https:\/\/ghproxy.com\//}" "${branch}"
|
|
||||||
|
local formatUrl="$url"
|
||||||
|
if [[ -d ${repo_path}/.git ]]; then
|
||||||
|
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
|
||||||
git_pull_scripts ${repo_path} "${branch}"
|
git_pull_scripts ${repo_path} "${branch}"
|
||||||
else
|
else
|
||||||
git_clone_scripts ${url} ${repo_path} "${branch}"
|
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
|
||||||
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
|
else
|
||||||
echo -e "\n更新${repo_path}失败,请检查原因...\n"
|
echo -e "\n更新${repo_path}失败,请检查网络...\n"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新所有 raw 文件
|
## 更新所有 raw 文件
|
||||||
update_raw() {
|
update_raw() {
|
||||||
echo -e "--------------------------------------------------------------\n"
|
echo -e "--------------------------------------------------------------\n"
|
||||||
local raw_url="$1"
|
local url="$1"
|
||||||
raw_file_name=$(echo ${raw_url} | awk -F "/" '{print $NF}')
|
local raw_url="$url"
|
||||||
|
local suffix="${raw_url##*.}"
|
||||||
|
local raw_file_name="${uniq_path}.${suffix}"
|
||||||
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
|
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
|
||||||
|
|
||||||
|
set_proxy
|
||||||
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
|
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
|
||||||
|
unset_proxy
|
||||||
|
|
||||||
if [[ $? -eq 0 ]]; then
|
if [[ $? -eq 0 ]]; then
|
||||||
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
|
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
|
||||||
echo -e "下载 ${raw_file_name} 成功...\n"
|
echo -e "下载 ${raw_file_name} 成功...\n"
|
||||||
cd $dir_raw
|
cd $dir_raw
|
||||||
local filename="raw_${raw_file_name}"
|
local filename="raw_${raw_file_name}"
|
||||||
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1)
|
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
|
||||||
cp -f $raw_file_name $dir_scripts/${filename}
|
cp -f $raw_file_name $dir_scripts/${filename}
|
||||||
cron_line=$(
|
cron_line=$(
|
||||||
perl -ne "{
|
perl -ne "{
|
||||||
@@ -209,17 +200,18 @@ update_raw() {
|
|||||||
)
|
)
|
||||||
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
||||||
[[ -z $cron_name ]] && cron_name="$raw_file_name"
|
[[ -z $cron_name ]] && cron_name="$raw_file_name"
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line="0 6 * * *"
|
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||||
|
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||||
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"
|
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
|
||||||
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
|
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
|
||||||
[ -f "$dir_raw/${raw_file_name}.new" ] && rm -f "$dir_raw/${raw_file_name}.new"
|
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -227,7 +219,7 @@ update_raw() {
|
|||||||
## 调用用户自定义的extra.sh
|
## 调用用户自定义的extra.sh
|
||||||
run_extra_shell() {
|
run_extra_shell() {
|
||||||
if [[ ${EnableExtraShell} == true ]]; then
|
if [[ ${EnableExtraShell} == true ]]; then
|
||||||
if [ -f $file_extra_shell ]; then
|
if [[ -f $file_extra_shell ]]; then
|
||||||
echo -e "--------------------------------------------------------------\n"
|
echo -e "--------------------------------------------------------------\n"
|
||||||
. $file_extra_shell
|
. $file_extra_shell
|
||||||
else
|
else
|
||||||
@@ -239,81 +231,113 @@ run_extra_shell() {
|
|||||||
## 脚本用法
|
## 脚本用法
|
||||||
usage() {
|
usage() {
|
||||||
echo -e "本脚本用法:"
|
echo -e "本脚本用法:"
|
||||||
echo -e "1. $cmd_update update # 更新并重启青龙"
|
echo -e "1. $cmd_update update # 更新并重启青龙"
|
||||||
echo -e "1. $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 code # 获取互助码"
|
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 reset # 重置青龙基础环境"
|
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
|
||||||
|
echo -e "9. $cmd_update resettfa # 禁用两步登录"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新qinglong
|
## 更新qinglong
|
||||||
update_qinglong() {
|
update_qinglong() {
|
||||||
|
patch_version
|
||||||
|
|
||||||
local no_restart="$1"
|
local no_restart="$1"
|
||||||
echo -e "--------------------------------------------------------------\n"
|
local all_branch=$(git branch -a)
|
||||||
[ -f $dir_root/package.json ] && ql_depend_old=$(cat $dir_root/package.json)
|
local primary_branch="master"
|
||||||
reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git"
|
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
|
||||||
git_pull_scripts $dir_root
|
primary_branch="${current_branch}"
|
||||||
|
fi
|
||||||
|
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
|
||||||
|
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_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
|
||||||
|
|
||||||
[ -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="${github_proxy_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}
|
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
|
||||||
cd ${ql_static_repo}
|
git_pull_scripts ${ql_static_repo} ${primary_branch}
|
||||||
git fetch --all
|
|
||||||
exit_status=$?
|
|
||||||
git reset --hard origin/master
|
|
||||||
cd $dir_root
|
|
||||||
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"
|
||||||
cd $ql_static_repo
|
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
|
||||||
commit_id=$(git rev-parse --short HEAD)
|
echo -e "\n当前版本 $static_version...\n"
|
||||||
echo -e "\n当前静态资源版本 $commit_id...\n"
|
|
||||||
cd $dir_root
|
rm -rf $dir_static/*
|
||||||
rm -rf $dir_root/build && rm -rf $dir_root/dist
|
cp -rf $ql_static_repo/* $dir_static
|
||||||
cp -rf $ql_static_repo/* $dir_root
|
|
||||||
if [[ $no_restart != "no-restart" ]]; then
|
if [[ $no_restart != "no-restart" ]]; then
|
||||||
echo -e "重启面板中..."
|
|
||||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||||
sleep 1
|
echo -e "重启面板中..."
|
||||||
|
sleep 3
|
||||||
reload_pm2
|
reload_pm2
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "\n更新$dir_root失败,请检查原因...\n"
|
echo -e "\n更新青龙静态资源失败,请检查原因...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reload_pm2() {
|
patch_version() {
|
||||||
pm2 l >/dev/null 2>&1
|
if [[ -f "$dir_root/db/cookie.db" ]]; then
|
||||||
|
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
||||||
if [[ $(pm2 info panel 2>/dev/null) ]]; then
|
mv $dir_root/db/cookie.db $dir_root/db/env.db
|
||||||
pm2 reload panel >/dev/null 2>&1
|
rm -rf $dir_root/db/cookie.db
|
||||||
else
|
echo
|
||||||
pm2 start $dir_root/build/app.js -n panel >/dev/null 2>&1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $(pm2 info schedule 2>/dev/null) ]]; then
|
if ! type ts-node &>/dev/null; then
|
||||||
pm2 reload schedule >/dev/null 2>&1
|
pnpm i -g ts-node typescript tslib
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule >/dev/null 2>&1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 兼容pnpm@7
|
||||||
|
pnpm setup
|
||||||
|
source ~/.bashrc
|
||||||
|
|
||||||
|
git config --global pull.rebase false
|
||||||
|
|
||||||
|
cp -f $dir_root/.env.example $dir_root/.env
|
||||||
|
|
||||||
|
if [[ -d "$dir_root/db" ]]; then
|
||||||
|
echo -e "检测到旧的db目录,拷贝到data目录...\n"
|
||||||
|
cp -rf $dir_root/config $dir_root/data
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$dir_root/scripts" ]]; then
|
||||||
|
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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## 对比脚本
|
## 对比脚本
|
||||||
@@ -324,24 +348,24 @@ 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 repo="${repo_path##*/}"
|
local list_add="$dir_list_tmp/${uniq_path}_add.list"
|
||||||
local list_add="$dir_list_tmp/${repo}_add.list"
|
local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
|
||||||
local list_drop="$dir_list_tmp/${repo}_drop.list"
|
diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
|
||||||
diff_cron "$dir_list_tmp/${repo}_scripts.list" "$dir_list_tmp/${repo}_user.list" $list_add $list_drop
|
|
||||||
|
|
||||||
if [ -s $list_drop ]; then
|
if [[ -s $list_drop ]]; then
|
||||||
output_list_add_drop $list_drop "失效"
|
output_list_add_drop $list_drop "失效"
|
||||||
if [[ ${AutoDelCron} == true ]]; then
|
if [[ ${AutoDelCron} == true ]]; then
|
||||||
del_cron $list_drop $repo
|
del_cron $list_drop $uniq_path
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -s $list_add ]; then
|
if [[ -s $list_add ]]; then
|
||||||
output_list_add_drop $list_add "新"
|
output_list_add_drop $list_add "新"
|
||||||
if [[ ${AutoAddCron} == true ]]; then
|
if [[ ${AutoAddCron} == true ]]; then
|
||||||
add_cron $list_add $repo
|
add_cron $list_add $uniq_path
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
@@ -356,13 +380,18 @@ gen_list_repo() {
|
|||||||
local blackword="$4"
|
local blackword="$4"
|
||||||
local dependence="$5"
|
local dependence="$5"
|
||||||
|
|
||||||
local repo="${repo_path##*/}"
|
rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
|
||||||
rm -f $dir_list_tmp/${repo}*.list >/dev/null 2>&1
|
|
||||||
|
|
||||||
cd ${repo_path}
|
cd ${repo_path}
|
||||||
|
|
||||||
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}\""
|
||||||
@@ -378,77 +407,145 @@ gen_list_repo() {
|
|||||||
if [[ $blackword ]]; then
|
if [[ $blackword ]]; then
|
||||||
files=$(echo "$files" | egrep -v $blackword)
|
files=$(echo "$files" | egrep -v $blackword)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
|
||||||
|
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
|
||||||
|
|
||||||
if [[ $dependence ]]; then
|
if [[ $dependence ]]; then
|
||||||
eval $cmd | sed 's/^..//' | egrep $dependence | xargs -i cp {} $dir_scripts
|
cd ${repo_path}
|
||||||
|
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
|
||||||
|
for _file in ${results}; do
|
||||||
|
file_path=$(dirname $_file)
|
||||||
|
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
|
||||||
|
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -d $dir_dep ]]; then
|
||||||
|
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
for file in ${files}; do
|
for file in ${files}; do
|
||||||
filename=$(basename $file)
|
filename=$(basename $file)
|
||||||
cp -f $file $dir_scripts/${repo}_${filename}
|
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
|
||||||
echo ${repo}_${filename} >>"$dir_list_tmp/${repo}_scripts.list"
|
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
|
||||||
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${author}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${author}_${filename}\.*|\1|" | head -1)
|
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
if [[ $cron_id ]]; then
|
if [[ $cron_id ]]; then
|
||||||
result=$(update_cron_command_api "$cmd_task ${repo}_${filename}:$cron_id")
|
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
grep -E "$cmd_task $repo" $list_crontab_user | perl -pe "s|.*ID=(.*) $cmd_task ($repo_.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${repo}_user.list"
|
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_uniq_path() {
|
||||||
|
local url="$1"
|
||||||
|
local branch="$2"
|
||||||
|
local urlTmp="${url%*/}"
|
||||||
|
local repoTmp="${urlTmp##*/}"
|
||||||
|
local repo="${repoTmp%.*}"
|
||||||
|
local tmp="${url%/*}"
|
||||||
|
local authorTmp1="${tmp##*/}"
|
||||||
|
local authorTmp2="${authorTmp1##*:}"
|
||||||
|
local author="${authorTmp2##*.}"
|
||||||
|
|
||||||
|
uniq_path="${author}_${repo}"
|
||||||
|
[[ $branch ]] && uniq_path="${uniq_path}_${branch}"
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
## for ql update
|
||||||
|
show_log="false"
|
||||||
|
while getopts ":l" opt
|
||||||
|
do
|
||||||
|
case $opt in
|
||||||
|
l)
|
||||||
|
show_log="true"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
||||||
|
|
||||||
local p1=$1
|
local p1=$1
|
||||||
local p2=$2
|
local p2=$2
|
||||||
local p3=$3
|
local p3=$3
|
||||||
local p4=$4
|
local p4=$4
|
||||||
local p5=$5
|
local p5=$5
|
||||||
local p6=$6
|
local p6=$6
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
local p7=$7
|
||||||
log_path="$dir_log/update/${log_time}_$p1.log"
|
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||||
|
local log_path="$dir_log/update/${log_time}_$p1.log"
|
||||||
|
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
case $p1 in
|
case $p1 in
|
||||||
update)
|
update)
|
||||||
update_qinglong "$2" | tee $log_path
|
cmd=">> $log_path 2>&1"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
|
eval echo -e "## 开始执行... $begin_time\n" $cmd
|
||||||
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
eval update_qinglong "$2" $cmd
|
||||||
;;
|
;;
|
||||||
extra)
|
extra)
|
||||||
run_extra_shell | tee -a $log_path
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
|
run_extra_shell >>$log_path
|
||||||
;;
|
;;
|
||||||
repo)
|
repo)
|
||||||
get_user_info
|
get_uniq_path "$p2" "$p6"
|
||||||
local name=$(echo "${p2##*/}" | awk -F "." '{print $1}')
|
|
||||||
log_path="$dir_log/update/${log_time}_$name.log"
|
|
||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" | tee $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"
|
||||||
local name=$(echo "${p2##*/}" | awk -F "." '{print $1}')
|
|
||||||
log_path="$dir_log/update/${log_time}_$name.log"
|
|
||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_raw "$p2" | tee $log_path
|
update_raw "$p2"
|
||||||
else
|
else
|
||||||
echo -e "命令输入错误...\n"
|
echo -e "命令输入错误...\n"
|
||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
rmlog)
|
rmlog)
|
||||||
. $dir_shell/rmlog.sh "$p2" | tee $log_path
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
;;
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
code)
|
. $dir_shell/rmlog.sh "$p2" >>$log_path
|
||||||
. $dir_shell/code.sh
|
|
||||||
;;
|
;;
|
||||||
bot)
|
bot)
|
||||||
. $dir_shell/bot.sh
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
|
. $dir_shell/bot.sh >>$log_path
|
||||||
;;
|
;;
|
||||||
reset)
|
check)
|
||||||
. $dir_shell/reset.sh
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
|
. $dir_shell/check.sh >>$log_path
|
||||||
|
;;
|
||||||
|
resetlet)
|
||||||
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
|
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
|
||||||
|
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
|
||||||
|
echo "$auth_value" >$file_auth_user
|
||||||
|
;;
|
||||||
|
resettfa)
|
||||||
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
|
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
|
||||||
|
echo -e "禁用两步验证成功 \n $auth_value" >>$log_path
|
||||||
|
echo "$auth_value" >$file_auth_user
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e "命令输入错误...\n"
|
echo -e "命令输入错误...\n"
|
||||||
usage
|
usage
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
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
|
||||||
|
cat $log_path
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
import { history } from 'umi';
|
|
||||||
import { request } from '@/utils/http';
|
|
||||||
import config from '@/utils/config';
|
|
||||||
|
|
||||||
const titleMap: any = {
|
|
||||||
'/': '控制面板',
|
|
||||||
'/login': '登录',
|
|
||||||
'/crontab': '定时任务',
|
|
||||||
'/cookie': 'Session管理',
|
|
||||||
'/config': '配置文件',
|
|
||||||
'/diy': '自定义脚本',
|
|
||||||
'/diff': '对比工具',
|
|
||||||
'/log': '日志',
|
|
||||||
'/setting': '系统设置',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function render(oldRender: any) {
|
|
||||||
request
|
|
||||||
.get(`${config.apiPrefix}user`)
|
|
||||||
.then((data) => {
|
|
||||||
if (data.data && data.data.username) {
|
|
||||||
return oldRender();
|
|
||||||
}
|
|
||||||
localStorage.removeItem(config.authKey);
|
|
||||||
history.push('/login');
|
|
||||||
oldRender();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
if (e.response && e.response.status === 401) {
|
|
||||||
localStorage.removeItem(config.authKey);
|
|
||||||
history.push('/login');
|
|
||||||
oldRender();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onRouteChange({ matchedRoutes }: any) {
|
|
||||||
if (matchedRoutes.length) {
|
|
||||||
const path: string = matchedRoutes[matchedRoutes.length - 1].route.path;
|
|
||||||
document.title = `${titleMap[path]} - 控制面板`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,7 @@
|
|||||||
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
|
|
||||||
|
const NewPageLoading = () => {
|
||||||
|
return <PageLoading delay={1}></PageLoading>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewPageLoading;
|
||||||
@@ -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;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user