mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db564d1add | ||
|
|
4f422c0658 | ||
|
|
f267720af4 | ||
|
|
4a0c66bcc0 | ||
|
|
ef7283a9fd | ||
|
|
4498449eea | ||
|
|
ebb9676a51 | ||
|
|
6fb39ce835 | ||
|
|
59c26d90d3 | ||
|
|
1d2df860e8 | ||
|
|
ce22cad5b2 | ||
|
|
dc44ce6b1a | ||
|
|
b27ee23cc3 | ||
|
|
083c8869aa | ||
|
|
6a971a0d6e | ||
|
|
a25bfb6912 | ||
|
|
f19dd21155 | ||
|
|
79b342d1d2 | ||
|
|
20b0d75b0d | ||
|
|
c89724906e | ||
|
|
c3a548ece3 | ||
|
|
b431e39f8f | ||
|
|
710746809b | ||
|
|
8a225e9beb | ||
|
|
a5ecb204b2 | ||
|
|
f1f009da3b | ||
|
|
7291098f98 | ||
|
|
666545ff03 | ||
|
|
81e2bbb14c | ||
|
|
970dff144c | ||
|
|
819b15b15e | ||
|
|
3fa57c69ac | ||
|
|
18689c8119 | ||
|
|
61e7049baa | ||
|
|
39c40a328d | ||
|
|
955d815d14 | ||
|
|
3a3b945de8 | ||
|
|
73c53dbc8d | ||
|
|
f76828d4ec | ||
|
|
7b2956eb0a | ||
|
|
b2167ae5e4 | ||
|
|
98dfa4c425 | ||
|
|
5bf08b76de | ||
|
|
5c5b4f804e | ||
|
|
6455245d11 | ||
|
|
2ff3d9c601 | ||
|
|
d342fbf29f | ||
|
|
f865018cd6 | ||
|
|
3b9a4f0834 | ||
|
|
67bc305950 | ||
|
|
97f128d59f | ||
|
|
e25885c4d9 | ||
|
|
c285026339 | ||
|
|
6c34045a48 | ||
|
|
e1e6261b4f | ||
|
|
697bcb5922 | ||
|
|
e05b0a2491 | ||
|
|
e1655455d8 | ||
|
|
9921dab064 | ||
|
|
403ab7f0f0 | ||
|
|
9bc7ca4094 | ||
|
|
e924aa76b0 | ||
|
|
0c46606e14 | ||
|
|
dbfa4049f5 | ||
|
|
622fe2a8f8 | ||
|
|
7bce5c4f6a | ||
|
|
1f7f2c8971 | ||
|
|
437a849c2a | ||
|
|
8f7261dd50 | ||
|
|
76fa82c3a7 | ||
|
|
46c9774efa | ||
|
|
1bc64a23e3 | ||
|
|
cc9a17afa3 | ||
|
|
fcbcfc3a03 | ||
|
|
0144f3b131 | ||
|
|
518436d5ca | ||
|
|
1961a6b913 | ||
|
|
5edebdd5f3 | ||
|
|
48edf2c212 | ||
|
|
9b16dca9da | ||
|
|
9479c2498c | ||
|
|
4a18bcabe2 | ||
|
|
34f690c53d | ||
|
|
e70cd6579f | ||
|
|
99c047e9c7 | ||
|
|
978ae326f8 | ||
|
|
b01f27f2d3 | ||
|
|
4d34e0dd96 | ||
|
|
9b0065e533 | ||
|
|
34a51feee9 | ||
|
|
679091de59 | ||
|
|
c6220cb920 | ||
|
|
633b30269c | ||
|
|
8ad26267d8 | ||
|
|
7cfb125682 | ||
|
|
a2dc5de5ce | ||
|
|
4f41ff29ec | ||
|
|
13d95b8c19 | ||
|
|
335c13ab44 | ||
|
|
015cdc10a7 | ||
|
|
a5075ab28f | ||
|
|
2e3c3274c7 | ||
|
|
443c581955 | ||
|
|
5e41faf590 | ||
|
|
0579fb83d3 | ||
|
|
2864845d5b | ||
|
|
659dddace7 | ||
|
|
b096f30558 | ||
|
|
dcfb72143a | ||
|
|
c63bcd7706 | ||
|
|
bac306f9b3 | ||
|
|
a6fd96e97b | ||
|
|
7edd91f923 | ||
|
|
66df445bd5 | ||
|
|
5c03034bb4 | ||
|
|
0eab181d46 | ||
|
|
f6f95308c7 | ||
|
|
e93cad91b4 | ||
|
|
08ee61b179 | ||
|
|
0ab756665e | ||
|
|
3570cddce0 | ||
|
|
a2d9f1a5db | ||
|
|
0c6a214e55 | ||
|
|
095fcdbe69 | ||
|
|
3afb1d18f6 | ||
|
|
4526c84330 | ||
|
|
a9cc1cb4b9 | ||
|
|
7acf1eace3 | ||
|
|
bb2e8cb287 | ||
|
|
3b389259c1 | ||
|
|
310ce55b09 | ||
|
|
4662e3fec2 | ||
|
|
10fab9d415 | ||
|
|
acecf01cbe | ||
|
|
b95fb9cda4 | ||
|
|
23fd595582 | ||
|
|
468d05cb10 | ||
|
|
88c2e5d160 | ||
|
|
8cf491b802 | ||
|
|
5886e78299 | ||
|
|
591ef9781c | ||
|
|
52fef96c1c | ||
|
|
d5fd9c57ec | ||
|
|
8a7d6a1c13 | ||
|
|
1745c72981 | ||
|
|
b98b7646cb | ||
|
|
1446e925ec | ||
|
|
799cda1e3e | ||
|
|
d2e0231341 | ||
|
|
9e0aa655a0 | ||
|
|
e303c73d44 | ||
|
|
e5e8e66d6b | ||
|
|
0bb3381122 | ||
|
|
2c5bf2bd6b | ||
|
|
83763b00ae | ||
|
|
47fc795c97 | ||
|
|
6a59bf7af3 | ||
|
|
6f185570d6 | ||
|
|
8b8336d9d0 | ||
|
|
a1e1d6ed3f | ||
|
|
df3e209534 | ||
|
|
851408860e | ||
|
|
d92ea23b0e | ||
|
|
d83641db92 | ||
|
|
48f262e1f3 | ||
|
|
bc49a7db50 | ||
|
|
49975b461c | ||
|
|
3ccb635956 | ||
|
|
074a90e0ac | ||
|
|
526f927436 | ||
|
|
fae7fad915 | ||
|
|
56132ceb8b | ||
|
|
3c3fce0a5a | ||
|
|
8139384961 | ||
|
|
734cc94048 | ||
|
|
91dbb7770d | ||
|
|
707709837d | ||
|
|
e868e65208 | ||
|
|
b19ad2a13b | ||
|
|
98ccccf6ab | ||
|
|
0a6166c557 | ||
|
|
5ee00e52a8 | ||
|
|
ff2b4e0b2f | ||
|
|
8fdd8db51b | ||
|
|
f113eb7ba8 | ||
|
|
40bc25ed5f | ||
|
|
cd3eab5d35 | ||
|
|
7038e15ad2 | ||
|
|
c72abd29ec | ||
|
|
5efc3d2228 | ||
|
|
8ac3f83c79 | ||
|
|
b31b054d0c | ||
|
|
fb4a87f5ce | ||
|
|
049c73880b | ||
|
|
7b2c54f6a6 | ||
|
|
23bda39812 | ||
|
|
90af5801ee | ||
|
|
543c241bc6 | ||
|
|
19a58a3510 | ||
|
|
73c6fd18ed | ||
|
|
409297c6aa | ||
|
|
4272b9ea35 | ||
|
|
3c5c5b0bbc | ||
|
|
802b2d722e | ||
|
|
8f90d3f8ff | ||
|
|
f93cbbf508 | ||
|
|
2ba8756bd3 | ||
|
|
8013de56c9 | ||
|
|
4af9b22442 | ||
|
|
8a392170cf | ||
|
|
6385e053ae | ||
|
|
2789119882 | ||
|
|
3e0553c3e8 | ||
|
|
5e16ace1e7 | ||
|
|
0ca23cc4ca | ||
|
|
4949457001 | ||
|
|
8ae13fe823 | ||
|
|
fdf672092a | ||
|
|
48dd6ea826 | ||
|
|
68bb4f15e2 | ||
|
|
9c6d9ec22d | ||
|
|
ac075ace73 | ||
|
|
eb58774ee9 | ||
|
|
04613bf9a9 | ||
|
|
4194f1964d | ||
|
|
42c64c82d7 | ||
|
|
d44a8966f2 | ||
|
|
ff44330aa1 | ||
|
|
ed259e8168 | ||
|
|
444836975d | ||
|
|
3756fa93e4 | ||
|
|
064b52b306 | ||
|
|
648dc51474 | ||
|
|
7c0bc32759 | ||
|
|
d4b6cc5e4d | ||
|
|
75fca715e3 | ||
|
|
6e9a4a29ef | ||
|
|
78f643c50f | ||
|
|
2db4f1ba87 | ||
|
|
5741dbb78c | ||
|
|
88cf671465 | ||
|
|
29f082dea2 | ||
|
|
9a55968ed6 | ||
|
|
6ec6e25855 | ||
|
|
af48bc378b | ||
|
|
3f5ae4bcb9 | ||
|
|
c6624e9001 | ||
|
|
5530ce76e3 | ||
|
|
654b51e476 | ||
|
|
6428ac3624 | ||
|
|
c36450f436 | ||
|
|
dcea231249 | ||
|
|
f236119bd6 | ||
|
|
cb870fad85 | ||
|
|
44e2caedfc | ||
|
|
c0b5192296 | ||
|
|
ba43882c0b | ||
|
|
dfc706e16d | ||
|
|
25b03d4345 | ||
|
|
4e8f36d9a4 | ||
|
|
8f4d67ffa7 | ||
|
|
ad47277149 | ||
|
|
98c2a37ab6 | ||
|
|
f2fea47336 | ||
|
|
e274d3e2f9 | ||
|
|
36f4c3c02c | ||
|
|
ae891f8e55 | ||
|
|
c874dc9705 | ||
|
|
23bfbeb995 | ||
|
|
a75df03243 | ||
|
|
e44a2b5995 | ||
|
|
f23e7a9e49 | ||
|
|
e08f0cd308 | ||
|
|
1ebcfd8001 | ||
|
|
5168b044a3 | ||
|
|
931638b687 | ||
|
|
4d9bbcfd3f | ||
|
|
b0e3b26369 | ||
|
|
1e55f4065d | ||
|
|
518e8903a3 | ||
|
|
6e56a55f6f | ||
|
|
967071ad4e | ||
|
|
d23fcfaa5a | ||
|
|
a2f2306430 | ||
|
|
144b543091 | ||
|
|
e4ad8f512e | ||
|
|
591a5006da | ||
|
|
281ed6cfba | ||
|
|
7354a61184 | ||
|
|
04037f29e5 | ||
|
|
6b124bad89 | ||
|
|
54d99a4954 | ||
|
|
9947594366 | ||
|
|
d7ff934336 | ||
|
|
dc27856441 | ||
|
|
bd28682769 | ||
|
|
2060e62da4 | ||
|
|
70eac1e747 | ||
|
|
6f5a5fe066 | ||
|
|
8d24949202 | ||
|
|
f7b6237355 | ||
|
|
daf6f94c51 | ||
|
|
687d16e21b | ||
|
|
70571a9d34 | ||
|
|
4da8d8fe1e | ||
|
|
952cdc0e3f | ||
|
|
2de189d189 | ||
|
|
3edc6e83d7 | ||
|
|
31a5386cfe | ||
|
|
0198ebbbff | ||
|
|
ce3df8704d | ||
|
|
1b4ca3a684 | ||
|
|
e31c2c0955 | ||
|
|
4e389865b5 | ||
|
|
e5d8adf955 | ||
|
|
9a3181bc44 | ||
|
|
2f05c95422 | ||
|
|
bc5a3a2028 | ||
|
|
1fe91508f6 | ||
|
|
b9253c8191 | ||
|
|
7516acbc41 | ||
|
|
3b3d45da29 | ||
|
|
918d68d140 | ||
|
|
a3f56be299 | ||
|
|
f8f63890e5 | ||
|
|
9e997410ab | ||
|
|
529880642f | ||
|
|
d2590edab3 | ||
|
|
32eec68278 | ||
|
|
05ed8c9f4b | ||
|
|
bc281ee4d7 | ||
|
|
3e88314d0a | ||
|
|
141defd845 | ||
|
|
b9e49b181a | ||
|
|
102e447f78 | ||
|
|
f3de8435f1 | ||
|
|
06723407d2 | ||
|
|
db8dfb32cf | ||
|
|
a7117e4442 | ||
|
|
53414e5d70 | ||
|
|
7bbc776759 | ||
|
|
f123c0bfe1 | ||
|
|
830865ff77 | ||
|
|
15fc6f975c | ||
|
|
4582711867 | ||
|
|
1eb64720d1 | ||
|
|
29f5134fb4 | ||
|
|
55ffd85d48 | ||
|
|
075165f89d | ||
|
|
42dabbf4c0 | ||
|
|
999b5d325f | ||
|
|
027c3f584c | ||
|
|
2d3ebe6087 | ||
|
|
f016e6734a | ||
|
|
9f0503f138 | ||
|
|
f74821895e | ||
|
|
872519a213 | ||
|
|
dcd39ea6d8 | ||
|
|
a4a76ec235 | ||
|
|
87a265191c | ||
|
|
6f4736bddf | ||
|
|
e85742a293 | ||
|
|
28ea160004 | ||
|
|
99d881fc0d | ||
|
|
5d6ac7f07c | ||
|
|
a23dbfe5e1 | ||
|
|
2d818a19d1 | ||
|
|
be89fd4ff7 | ||
|
|
6d2edaaaf8 | ||
|
|
19afbd06c0 | ||
|
|
718964ab86 | ||
|
|
0dadd4e6cc | ||
|
|
57e7d756cb | ||
|
|
9f5fb30334 | ||
|
|
ec8c339f6e | ||
|
|
728d84cb15 | ||
|
|
a7b5a0f4c5 | ||
|
|
b009d3e3fd | ||
|
|
559defc8d6 | ||
|
|
98cc983473 | ||
|
|
6d42a680b1 | ||
|
|
1602f829ba | ||
|
|
c4a4764762 | ||
|
|
ac32117e78 | ||
|
|
3b5bcf8f16 | ||
|
|
8735321e34 | ||
|
|
be76ff4675 | ||
|
|
15678b029c | ||
|
|
05d47be1c8 | ||
|
|
106b34e33a | ||
|
|
488fbd2bbb | ||
|
|
b82c630e3f | ||
|
|
5a01c41bbb | ||
|
|
471e778a61 | ||
|
|
68e576a5e3 | ||
|
|
8c74fca362 | ||
|
|
026fc43695 | ||
|
|
4f925b92d0 | ||
|
|
4200d5e530 | ||
|
|
32ca72133a | ||
|
|
7708638267 | ||
|
|
cb24d3c9fa | ||
|
|
07c63e285e | ||
|
|
a5e8d5cb80 | ||
|
|
8892e8833e | ||
|
|
25af639eef | ||
|
|
58383e9022 | ||
|
|
d244650432 | ||
|
|
f338537ea0 | ||
|
|
8d2c62d6d1 | ||
|
|
8b1906c786 | ||
|
|
3f29c1aa4e | ||
|
|
31ff0476a6 | ||
|
|
b3ed824b2e | ||
|
|
ec97242415 | ||
|
|
d54faf8044 | ||
|
|
5f380b55d1 | ||
|
|
8d46115823 | ||
|
|
cc24e51cd7 | ||
|
|
7a5e6dba51 | ||
|
|
ad1ee0d2ef | ||
|
|
e4f7461b62 | ||
|
|
a9e0a934a7 | ||
|
|
2a23b838f6 | ||
|
|
1afc791e0b | ||
|
|
05cf51afa0 | ||
|
|
76b6677f5a | ||
|
|
af74afd10d | ||
|
|
46512142fa | ||
|
|
6f13cc6e6f | ||
|
|
9dab6660d0 | ||
|
|
89ddfd20cd | ||
|
|
c9bd27ec4f | ||
|
|
2e937ad5ae | ||
|
|
39d3d53614 | ||
|
|
456b4c4136 | ||
|
|
a90e536403 | ||
|
|
d96206ef66 | ||
|
|
ef71e13139 | ||
|
|
189826c5db | ||
|
|
bdecdce118 | ||
|
|
460a1750f4 | ||
|
|
d2c2ea8d3b | ||
|
|
cb12d8ffec | ||
|
|
5bb1afe5c6 | ||
|
|
7caabe9063 | ||
|
|
1695b8c0fe | ||
|
|
0927682abf | ||
|
|
cde4646fe1 | ||
|
|
8c410377b9 | ||
|
|
312aaf18de | ||
|
|
ac2979381f | ||
|
|
03c5c7bbea | ||
|
|
513de1a792 | ||
|
|
c6a8b93b70 | ||
|
|
4c8e097448 | ||
|
|
4e7a5c637d | ||
|
|
f1a51638a5 | ||
|
|
9dcc547ac7 | ||
|
|
bb47d67d0b | ||
|
|
f6a122e5ea | ||
|
|
5523d537dc | ||
|
|
5a21247cbb | ||
|
|
419c5a7c5b | ||
|
|
468ff9c1af | ||
|
|
ebb6290468 | ||
|
|
85d8565241 | ||
|
|
bd35725b3c | ||
|
|
fb6a80e306 | ||
|
|
2c9b283b75 | ||
|
|
0218405998 | ||
|
|
aa59e9a44b | ||
|
|
f9e2e22b85 | ||
|
|
d27ca56a2d | ||
|
|
bf40447ba4 | ||
|
|
89f7b99a94 | ||
|
|
c71d34cbac | ||
|
|
b97ad5cb6b | ||
|
|
081793e1fb | ||
|
|
f76b7d3b35 | ||
|
|
95448418c0 | ||
|
|
199070f829 | ||
|
|
1fa296a53e | ||
|
|
caa8bd7e7f | ||
|
|
22d097aa08 | ||
|
|
5dbfc65e7f | ||
|
|
03e6f18e54 | ||
|
|
84357e9dcc | ||
|
|
8bb8a29400 | ||
|
|
1b893f35ba | ||
|
|
5be421b4ea | ||
|
|
8ad325686d | ||
|
|
54e58c1e08 | ||
|
|
8818a779b5 | ||
|
|
cc65b6c8bb | ||
|
|
523f583516 | ||
|
|
89539deff1 | ||
|
|
6907d520ed | ||
|
|
eae2d6d17a | ||
|
|
82dd9114fd | ||
|
|
58e047fe0e | ||
|
|
d703769d43 | ||
|
|
be6ca54d9c | ||
|
|
a5cadfbcab | ||
|
|
5e3be8fcd1 | ||
|
|
8b5347dd44 | ||
|
|
24f6070f9e | ||
|
|
d00f2cd33b | ||
|
|
8276a19012 | ||
|
|
c769c598b8 | ||
|
|
d687e79b6c | ||
|
|
8fac58d73f | ||
|
|
b2ba86b212 | ||
|
|
92673b4a17 | ||
|
|
85facc039a | ||
|
|
24f33431de | ||
|
|
36533ab7e2 | ||
|
|
97d170c8b1 | ||
|
|
1413999ac3 | ||
|
|
0d23f8a962 | ||
|
|
d0b13a04cc | ||
|
|
fca7b02656 | ||
|
|
2afbfa08c6 | ||
|
|
5fe63666b0 | ||
|
|
9ee08acf29 | ||
|
|
bc7f406968 | ||
|
|
00ee90792f | ||
|
|
4a8c90365e | ||
|
|
d0e46224dc | ||
|
|
54dd90e7c5 | ||
|
|
7ef3de1367 | ||
|
|
ce6aa23d5b | ||
|
|
dc819bcf8e | ||
|
|
b0595656bb | ||
|
|
1dac54b7e7 | ||
|
|
95ea085e1e | ||
|
|
ec5f6d20cb | ||
|
|
aa288c89b3 | ||
|
|
ea419a98ae | ||
|
|
0f0c79f52f | ||
|
|
38df66a5b1 | ||
|
|
0051260171 | ||
|
|
f6dc0e020f | ||
|
|
6f2e7b9459 | ||
|
|
384e3f09da | ||
|
|
747f4552e0 | ||
|
|
1db13653bf | ||
|
|
d05304f839 | ||
|
|
0b551ddc0d | ||
|
|
f117f6b39c | ||
|
|
0c54ca95d9 | ||
|
|
bcbc8a633f | ||
|
|
73963c89fc | ||
|
|
793d14f04b | ||
|
|
ef32181f36 | ||
|
|
419f59818b | ||
|
|
2bda3a276a | ||
|
|
5e5b28ef29 | ||
|
|
8dfbe93135 | ||
|
|
810c6ca76c | ||
|
|
912b4ddb76 | ||
|
|
6b41df3497 | ||
|
|
3e62fedc73 | ||
|
|
a10ffef38f | ||
|
|
816ce31279 | ||
|
|
4e58a640e4 | ||
|
|
43925d6630 | ||
|
|
b688461137 | ||
|
|
1e85528889 | ||
|
|
eb4976ed6b | ||
|
|
7dd87220ab | ||
|
|
9927461ea9 | ||
|
|
84c7ffc509 | ||
|
|
ee52f09bcb | ||
|
|
531fc48bdf | ||
|
|
0581a7ab10 | ||
|
|
f0d2d57584 | ||
|
|
cb154437ac | ||
|
|
7ea9a8aa57 | ||
|
|
7e34f4990d | ||
|
|
f0f2c6b537 | ||
|
|
f71b3d0378 | ||
|
|
7d6e1d3e3d | ||
|
|
cf5f1b6f25 | ||
|
|
903007e6ca | ||
|
|
d6614acf66 | ||
|
|
86df495b67 | ||
|
|
71b0c10997 | ||
|
|
e273c59d78 | ||
|
|
baea40f157 | ||
|
|
a809b89bb0 | ||
|
|
89e08953cb | ||
|
|
e154cf65b6 | ||
|
|
d7162a24be | ||
|
|
16532720d7 | ||
|
|
012ee2419d | ||
|
|
7e4fe2f1b3 | ||
|
|
5a95b72607 | ||
|
|
4d27055605 | ||
|
|
e2b9578253 | ||
|
|
57fd8f378c | ||
|
|
a726f224f8 | ||
|
|
d502ac6e0f | ||
|
|
f6a828f0af | ||
|
|
0a53a7001e | ||
|
|
0ab5494744 | ||
|
|
e60cf15f39 | ||
|
|
b53db97b98 | ||
|
|
f12c75f91b | ||
|
|
3df1a14f72 | ||
|
|
712f105d34 | ||
|
|
4ea757fece | ||
|
|
d8b32f63ea | ||
|
|
771b81afaf | ||
|
|
a4f4c6ac15 | ||
|
|
ae22cf064e | ||
|
|
fbb8e1bd31 | ||
|
|
47202ef66c | ||
|
|
f46c70dcbd | ||
|
|
c7a837e33d | ||
|
|
544e2e8331 | ||
|
|
a62ccbc0db | ||
|
|
891619ad55 | ||
|
|
a3a62f65dd | ||
|
|
3131f197b8 | ||
|
|
e13d9ed51b | ||
|
|
1b5f4a0f9d | ||
|
|
c9c4043a4e | ||
|
|
bb59dd977a | ||
|
|
c2b213a37c | ||
|
|
559f4418d1 | ||
|
|
2149bca7b8 | ||
|
|
be5db82b79 | ||
|
|
01163e3ea3 | ||
|
|
7f986fdb2d | ||
|
|
38e1ba67af | ||
|
|
d3d2e0887e | ||
|
|
a2650715b3 | ||
|
|
8c0685202e | ||
|
|
5b6843b5c0 | ||
|
|
ba341b69ac | ||
|
|
ca89f03724 | ||
|
|
5461325e69 | ||
|
|
e96a2818b7 | ||
|
|
355f06a3cf | ||
|
|
f6e6cfa71c | ||
|
|
9cdfa2ef79 | ||
|
|
6fab22099d | ||
|
|
925819b3aa | ||
|
|
6f96900ab9 | ||
|
|
43bc29b25c | ||
|
|
e13ff86ff6 | ||
|
|
3f3d572932 | ||
|
|
a0ce1562ac | ||
|
|
ee84cbca51 | ||
|
|
3ed5cb48b5 | ||
|
|
499ba5fb99 | ||
|
|
c2f036c370 | ||
|
|
326580b3c8 | ||
|
|
bf59351cf9 | ||
|
|
a954fc3942 | ||
|
|
160a66af23 | ||
|
|
4edbd29b19 | ||
|
|
b6b97478d1 | ||
|
|
27a871e537 | ||
|
|
738b0f9a4a | ||
|
|
1a56dd8ac6 | ||
|
|
672d440725 | ||
|
|
3625b7c0de | ||
|
|
e75a683173 | ||
|
|
89ed8527d6 | ||
|
|
5a5f4b8065 | ||
|
|
d2e23341eb | ||
|
|
233081149b | ||
|
|
5d19ee0ab5 | ||
|
|
653b1cef20 | ||
|
|
f3ecf6e7a0 | ||
|
|
35d8f0a7e8 | ||
|
|
e863dd322a | ||
|
|
fa6dec969f | ||
|
|
58cd05fd09 | ||
|
|
2e6ca5419d | ||
|
|
a29392bb08 | ||
|
|
4dc9c9aa75 | ||
|
|
03c9f79549 | ||
|
|
46aaeb4eac | ||
|
|
4d20bb0de6 | ||
|
|
e001686627 | ||
|
|
931f02ef8e | ||
|
|
a9be3e6376 | ||
|
|
aedd369fc4 | ||
|
|
a1016b7f22 | ||
|
|
603432aa87 | ||
|
|
cd23967ae0 | ||
|
|
dfb159c4ad | ||
|
|
0318f03283 | ||
|
|
9ddf6986e4 | ||
|
|
a28ed9281e | ||
|
|
9cc776aded | ||
|
|
349f14a7fc | ||
|
|
fae26efb88 | ||
|
|
64d0bb18b1 | ||
|
|
864ce071f6 | ||
|
|
5a55934029 | ||
|
|
1f061ad059 | ||
|
|
32f0774df2 | ||
|
|
3206ff6a1e | ||
|
|
2f5e946979 | ||
|
|
5a6d3739d4 | ||
|
|
fa018b84a7 | ||
|
|
a686206e21 | ||
|
|
6132d13261 | ||
|
|
f64c78919d | ||
|
|
1a1067966c | ||
|
|
ec545cabcd | ||
|
|
40b0e90c0d | ||
|
|
5e94be21aa | ||
|
|
417ae8bbde | ||
|
|
acde7ebef6 | ||
|
|
a424e8df7c | ||
|
|
2eed5cba14 | ||
|
|
19e7c238f6 | ||
|
|
f3c939e442 | ||
|
|
3bcf3431d3 | ||
|
|
20d6308bc7 | ||
|
|
96b0127811 | ||
|
|
68cc792d6c | ||
|
|
d0f7449b57 | ||
|
|
b41c1f1d2e | ||
|
|
9a8b72fe3f | ||
|
|
b16f8b96dc | ||
|
|
128429a6a9 | ||
|
|
b849d8b526 | ||
|
|
47b06c3487 | ||
|
|
40e0d143f4 | ||
|
|
c1269d9dde | ||
|
|
6a81ba9069 | ||
|
|
61883776f2 | ||
|
|
f7752cec3a | ||
|
|
6f96c84a16 | ||
|
|
a2ce3304de | ||
|
|
ee0b47d101 | ||
|
|
3b3dd4c26d | ||
|
|
bee1f9968e | ||
|
|
65691cf7c9 | ||
|
|
7b7bcf600d | ||
|
|
18cd7bfa6f | ||
|
|
8c14a31967 | ||
|
|
6c840b435e | ||
|
|
eca615c066 | ||
|
|
22bb785139 | ||
|
|
62fca3dfe2 | ||
|
|
9936097086 | ||
|
|
50dd235e39 | ||
|
|
606bac2e43 | ||
|
|
90ede97797 | ||
|
|
1545cde58a | ||
|
|
8481d81257 | ||
|
|
0688d4f318 | ||
|
|
ab5ab951d7 | ||
|
|
0ca640acbd | ||
|
|
235f99c0ca | ||
|
|
31fdf7f12c | ||
|
|
bd6d711212 | ||
|
|
62596f27ef | ||
|
|
1b88853a8d | ||
|
|
3c5925ff67 | ||
|
|
7d5687d504 | ||
|
|
6b74cccc34 | ||
|
|
1becb26aaf | ||
|
|
bd2917822e | ||
|
|
9c82db4471 | ||
|
|
e4f11156f6 | ||
|
|
c7de19d3b5 | ||
|
|
65ade4b996 | ||
|
|
2b70851074 | ||
|
|
b7b983229b | ||
|
|
ac548433c0 | ||
|
|
02a1afb477 | ||
|
|
e959466898 | ||
|
|
5c5aef64b0 | ||
|
|
86e8b1383c | ||
|
|
9f7bf1f6b8 | ||
|
|
2ff3869608 | ||
|
|
8d21401f88 | ||
|
|
b92daedea2 | ||
|
|
021d89379a | ||
|
|
2b61204233 | ||
|
|
1da1659b51 | ||
|
|
1302a6daa0 | ||
|
|
160dbe780c | ||
|
|
d047cf0d77 | ||
|
|
0c2a2af308 | ||
|
|
bd8953ea81 | ||
|
|
a479f0fbce | ||
|
|
2e00e80e96 | ||
|
|
0bf4aec74e | ||
|
|
bcac3deb6e | ||
|
|
596f93623e | ||
|
|
c74a1bc20c | ||
|
|
795d1b938d | ||
|
|
bad247c51c | ||
|
|
950aed3522 | ||
|
|
8a7bd53f24 | ||
|
|
04230cbd1c | ||
|
|
d0a7e67138 | ||
|
|
56517fd345 | ||
|
|
6315425a7b | ||
|
|
c42299766c | ||
|
|
ef9fa02145 | ||
|
|
51869b6a84 | ||
|
|
d12953a345 | ||
|
|
9f727cd54b | ||
|
|
3de567191e | ||
|
|
b212762417 | ||
|
|
d5f619ff74 | ||
|
|
87027f8a8c | ||
|
|
6fa8a678bf | ||
|
|
f2aa355389 | ||
|
|
b3ef432c6f | ||
|
|
860a1e4dc6 | ||
|
|
f5b322649d | ||
|
|
746650b49a | ||
|
|
43ddc54a70 | ||
|
|
bdfa08bb78 | ||
|
|
07e58a484c | ||
|
|
a652e1e190 | ||
|
|
ec0deeff06 | ||
|
|
7335e2c299 | ||
|
|
003cdd1e06 | ||
|
|
efa1fcfbdf | ||
|
|
7acda0c039 | ||
|
|
cb86fda169 | ||
|
|
324d106693 | ||
|
|
0644edb62e | ||
|
|
0f31cb5fcc | ||
|
|
c80a9d1e65 | ||
|
|
b26d0b4887 | ||
|
|
ed93b919ba | ||
|
|
b1077443a3 | ||
|
|
9455ca64a2 | ||
|
|
d6ebd2699e | ||
|
|
632514d36e | ||
|
|
686baa56f4 | ||
|
|
a3006af7b8 | ||
|
|
ec529bfca6 | ||
|
|
a661b6bd38 | ||
|
|
73e85c2def | ||
|
|
6819487a43 | ||
|
|
14b20873c7 | ||
|
|
bd1ca7e975 | ||
|
|
c01ec828e2 | ||
|
|
47abdb0f36 | ||
|
|
6d4568cdcd | ||
|
|
2068ef7053 | ||
|
|
5655591215 | ||
|
|
feb22f25b8 | ||
|
|
c054259806 | ||
|
|
b179649078 | ||
|
|
a75561399d | ||
|
|
fa02df0d8f | ||
|
|
f17642b243 | ||
|
|
43773222f8 | ||
|
|
91a5e13cf2 | ||
|
|
be4efbad5d | ||
|
|
da3e63e6c3 | ||
|
|
2d427c0670 | ||
|
|
97184d22c3 | ||
|
|
f1cc7641fb | ||
|
|
ca9d742359 | ||
|
|
1d82310ca1 | ||
|
|
687afce245 | ||
|
|
26d986d4a8 | ||
|
|
120599c778 | ||
|
|
54945f2183 | ||
|
|
f48b91dc0a | ||
|
|
4c1551c309 | ||
|
|
558478be16 | ||
|
|
4e403d3d8a | ||
|
|
a79fd74130 | ||
|
|
af2f3deeab | ||
|
|
c15ff89746 | ||
|
|
5b585e0cda | ||
|
|
6619374571 | ||
|
|
9fc8fdd9d1 | ||
|
|
900d279bf6 | ||
|
|
265760146f | ||
|
|
74fa8ffcd1 | ||
|
|
e5313e63a9 | ||
|
|
f482796cdf | ||
|
|
aa08f489d7 | ||
|
|
1cc54b5da6 | ||
|
|
0baf0d23ae | ||
|
|
646443deb5 | ||
|
|
39fb23c025 | ||
|
|
698c463c3a | ||
|
|
6c87634bb3 | ||
|
|
0a9e9e972d | ||
|
|
bb0ad6c325 | ||
|
|
f2dac51a62 | ||
|
|
9534cda1f9 | ||
|
|
27226e7222 | ||
|
|
c51621ee04 | ||
|
|
c78e4447fa | ||
|
|
2d7e525024 | ||
|
|
ecee9e2acd | ||
|
|
d607619f8f | ||
|
|
e8e275cd79 | ||
|
|
4147025406 | ||
|
|
35539a4ccc | ||
|
|
b7a359714f | ||
|
|
bf43bfbd92 | ||
|
|
1265e465d0 | ||
|
|
8f195b964f | ||
|
|
a285efdd85 | ||
|
|
fafb68dbf0 | ||
|
|
3dc87329f1 | ||
|
|
7fc15518b3 | ||
|
|
eb196bd40d | ||
|
|
0cbfca979e | ||
|
|
b3ac81b7b7 | ||
|
|
bb40111d38 | ||
|
|
29a3bea6e4 | ||
|
|
ab4f167bda | ||
|
|
035c69898f | ||
|
|
00cb915f48 | ||
|
|
97baff8baf | ||
|
|
e4da2edf56 | ||
|
|
c1e08ba9f0 | ||
|
|
f00566624d | ||
|
|
2bb55294fc | ||
|
|
86c3e9a843 | ||
|
|
3a998a37f0 | ||
|
|
7eed47cd8e | ||
|
|
8cef0b6216 | ||
|
|
930541d5d6 | ||
|
|
f3518d0cad | ||
|
|
45e2c43997 | ||
|
|
6c0f932ae8 | ||
|
|
5832bf9140 | ||
|
|
47bba19bec | ||
|
|
1e58254f4c | ||
|
|
7739cef7b8 | ||
|
|
9b73392c5e | ||
|
|
79e5b2fb84 | ||
|
|
8ce1a33bd3 | ||
|
|
8ae61f1b23 | ||
|
|
a973c93c04 | ||
|
|
9c0f14aa89 | ||
|
|
f78d72db4c | ||
|
|
fb0c1c6993 | ||
|
|
4d99695e4c | ||
|
|
18321943d6 | ||
|
|
67819d2817 | ||
|
|
ad74e014ef | ||
|
|
bf661dffcf | ||
|
|
11484175c8 | ||
|
|
7bc5004683 | ||
|
|
1d01508164 | ||
|
|
4cae9bcfb9 | ||
|
|
9c84c7bd38 | ||
|
|
5a5e81eae0 | ||
|
|
b0f6bb3ea6 | ||
|
|
0c004606b5 | ||
|
|
fa4978389c | ||
|
|
8b8359b08f | ||
|
|
1d4fec3979 | ||
|
|
bb4123805c | ||
|
|
f49f605c11 | ||
|
|
5496689d7c | ||
|
|
bad8bbdb7c | ||
|
|
76184227a4 | ||
|
|
bbc9ee120d | ||
|
|
85093b2856 | ||
|
|
8c355b6159 | ||
|
|
622037ee98 | ||
|
|
54bb13aad0 | ||
|
|
61e41269ba | ||
|
|
f7cffe3d2d | ||
|
|
0cd39f0c97 | ||
|
|
c09b22358b | ||
|
|
18215b8336 | ||
|
|
f0fae716cc | ||
|
|
7092be97eb | ||
|
|
d9a9d2ee4f | ||
|
|
f86892cef0 | ||
|
|
a48b915675 | ||
|
|
5e13264393 | ||
|
|
ff481ef7f3 | ||
|
|
82a4485792 | ||
|
|
538876f529 | ||
|
|
609a31455b | ||
|
|
2d6da9ebfe | ||
|
|
594e871744 | ||
|
|
95074eb028 |
+8
-4
@@ -1,9 +1,13 @@
|
|||||||
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=''
|
||||||
|
TEMP=''
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: "\U0001F41E Bug report"
|
||||||
|
description: Create a report to help us improve
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Qinglong version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: |
|
||||||
|
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
|
||||||
|
placeholder: Steps to reproduce
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: What is expected?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: actually-happening
|
||||||
|
attributes:
|
||||||
|
label: What is actually happening?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: system-info
|
||||||
|
attributes:
|
||||||
|
label: System Info
|
||||||
|
description: Output of `npx envinfo --system --binaries --browsers`
|
||||||
|
render: shell
|
||||||
|
placeholder: System, Binaries, Browsers
|
||||||
|
- type: textarea
|
||||||
|
id: additional-comments
|
||||||
|
attributes:
|
||||||
|
label: Any additional comments?
|
||||||
|
description: e.g. some background/context of how you ran into this bug.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Telegram Chat
|
||||||
|
url: https://t.me/jiao_long
|
||||||
|
about: Ask questions and discuss with other Qinglong users in real time.
|
||||||
|
- name: Questions & Discussions
|
||||||
|
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
|
||||||
|
about: Use GitHub discussions for message-board style questions and discussions.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: "\U0001F680 New feature proposal"
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
labels: [":sparkles: feature request"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for your interest in the project and taking the time to fill out this feature report!
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
attributes:
|
||||||
|
label: Clear and concise description of the problem
|
||||||
|
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: suggested-solution
|
||||||
|
attributes:
|
||||||
|
label: Suggested solution
|
||||||
|
description: "In module [xy] we could provide following implementation..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternative
|
||||||
|
attributes:
|
||||||
|
label: Alternative
|
||||||
|
description: Clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Any other context or screenshots about the feature request here.
|
||||||
|
- type: checkboxes
|
||||||
|
id: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Validations
|
||||||
|
description: Before submitting the issue, please make sure you do the following
|
||||||
|
options:
|
||||||
|
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## PR Type
|
||||||
|
What kind of change does this PR introduce?
|
||||||
|
|
||||||
|
<!-- Please check the one that applies to this PR using "x". -->
|
||||||
|
|
||||||
|
- [ ] Bugfix
|
||||||
|
- [ ] Feature
|
||||||
|
- [ ] Code style update (formatting, local variables)
|
||||||
|
- [ ] Refactoring (no functional changes, no api changes)
|
||||||
|
- [ ] Other... Please describe:
|
||||||
|
|
||||||
|
|
||||||
|
## What is the current behavior?
|
||||||
|
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||||
|
|
||||||
|
Issue Number: N/A
|
||||||
|
|
||||||
|
|
||||||
|
## What is the new behavior?
|
||||||
|
|
||||||
|
|
||||||
|
## Does this PR introduce a breaking change?
|
||||||
|
|
||||||
|
- [ ] Yes
|
||||||
|
- [ ] No
|
||||||
|
|
||||||
|
|
||||||
|
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||||
|
|
||||||
|
|
||||||
|
## Other information
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Comment to be posted to on PRs from first time contributors in your repository
|
||||||
|
newPRWelcomeComment: |
|
||||||
|
💖 Thanks for opening this pull request! 💖
|
||||||
|
Please be patient and we will get back to you as soon as we can.
|
||||||
|
# Comment to be posted to on pull requests merged by a first time user
|
||||||
|
firstPRMergeComment: >
|
||||||
|
Congrats on merging your first pull request! 🎉🎉🎉
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
name: BuildStatic
|
|
||||||
|
|
||||||
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,168 @@
|
|||||||
|
name: Build And Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
- 'develop'
|
||||||
|
tags:
|
||||||
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
schedule:
|
||||||
|
# 参考 https://jasonet.co/posts/scheduled-actions/
|
||||||
|
- cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
to_gitlab:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: pixta-dev/repository-mirroring-action@v1
|
||||||
|
with:
|
||||||
|
target_repo_url:
|
||||||
|
git@gitlab.com:whyour/qinglong.git
|
||||||
|
ssh_private_key:
|
||||||
|
${{ secrets.GITLAB_SSH_PK }}
|
||||||
|
|
||||||
|
to_gitee:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: pixta-dev/repository-mirroring-action@v1
|
||||||
|
with:
|
||||||
|
target_repo_url:
|
||||||
|
git@gitee.com:whyour/qinglong.git
|
||||||
|
ssh_private_key:
|
||||||
|
${{ secrets.GITLAB_SSH_PK }}
|
||||||
|
|
||||||
|
build-static:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: build front and back
|
||||||
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
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 }}
|
||||||
|
REPO_GITEE: git@gitee.com:whyour/qinglong-static.git
|
||||||
|
REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git
|
||||||
|
PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
||||||
|
run: |
|
||||||
|
mkdir -p tmp
|
||||||
|
cd ./tmp
|
||||||
|
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}
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
|
||||||
|
git remote add gitee "${REPO_GITEE}"
|
||||||
|
git remote add gitlab "${REPO_GITLAB}"
|
||||||
|
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||||
|
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||||
|
|
||||||
|
build:
|
||||||
|
if: github.ref != 'refs/heads/master'
|
||||||
|
|
||||||
|
needs: build-static
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Setup timezone
|
||||||
|
uses: zcong1993/setup-timezone@master
|
||||||
|
with:
|
||||||
|
timezone: Asia/Shanghai
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
|
SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
network: host
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||||
|
context: .
|
||||||
|
file: ./docker/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=whyour/qinglong:cache
|
||||||
|
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
name: PublishDockerImage
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# workflow_run:
|
|
||||||
# workflows:
|
|
||||||
# - BuildStatic
|
|
||||||
# branches:
|
|
||||||
# - master
|
|
||||||
# types:
|
|
||||||
# - completed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout base
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- name: Docker login
|
|
||||||
env:
|
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
|
|
||||||
|
|
||||||
- name: Docker buildx image and push on master branch
|
|
||||||
env:
|
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
run: |
|
|
||||||
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
|
|
||||||
+5
-3
@@ -8,8 +8,8 @@
|
|||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/dist
|
/static
|
||||||
/build
|
/data
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -21,9 +21,11 @@
|
|||||||
/.env.local
|
/.env.local
|
||||||
.env
|
.env
|
||||||
.history
|
.history
|
||||||
|
.version.ts
|
||||||
|
|
||||||
/config
|
/config
|
||||||
/log
|
/log
|
||||||
/db
|
/db
|
||||||
/manual_log
|
/manual_log
|
||||||
/scripts
|
/scripts
|
||||||
|
/bak
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
|
||||||
|
strict-peer-dependencies=false
|
||||||
@@ -1,21 +1,28 @@
|
|||||||
import { defineConfig } from 'umi';
|
import { defineConfig } from '@umijs/max';
|
||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
hash: true,
|
hash: true,
|
||||||
layout: false,
|
antd: {},
|
||||||
nodeModulesTransform: {
|
outputPath: 'static/dist',
|
||||||
type: 'none',
|
fastRefresh: true,
|
||||||
|
favicons: ['./images/favicon.svg'],
|
||||||
|
mfsu: {
|
||||||
|
strategy: 'eager',
|
||||||
},
|
},
|
||||||
fastRefresh: {},
|
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||||
favicon: 'https://qinglong.whyour.cn/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: any) => {
|
||||||
config.plugin('compression-webpack-plugin').use(
|
config.plugin('compression-webpack-plugin').use(
|
||||||
new CompressionPlugin({
|
new CompressionPlugin({
|
||||||
algorithm: 'gzip',
|
algorithm: 'gzip',
|
||||||
@@ -24,20 +31,13 @@ export default defineConfig({
|
|||||||
minRatio: 0.6,
|
minRatio: 0.6,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
}) as any,
|
||||||
externals: {
|
externals: {
|
||||||
react: 'window.React',
|
react: 'window.React',
|
||||||
'react-dom': 'window.ReactDOM',
|
'react-dom': 'window.ReactDOM',
|
||||||
darkreader: 'window.DarkReader',
|
|
||||||
codemirror: 'window.CodeMirror',
|
|
||||||
},
|
},
|
||||||
scripts: [
|
headScripts: [
|
||||||
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
'https://gw.alipayobjects.com/os/lib/react/18.2.0/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/18.2.0/umd/react-dom.production.min.js',
|
||||||
'https://cdn.jsdelivr.net/npm/darkreader@4.9.34/darkreader.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/lib/codemirror.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/shell/shell.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/python/python.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/javascript/javascript.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.
|
||||||
+218
@@ -0,0 +1,218 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/whyour/qinglong">
|
||||||
|
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">Green Dragon</h1>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||||
|
|
||||||
|
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||||
|
|
||||||
|
[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 refined, see the development steps first (not supported on windows yet)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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/data:/ql/data \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
docker.io/whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
1. docker installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -sSL get.docker.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. configure domestic mirror sources
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /etc/docker
|
||||||
|
tee /etc/docker/daemon.json <<-'EOF'
|
||||||
|
{
|
||||||
|
"registry-mirrors": [
|
||||||
|
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
||||||
|
"https://ypzju6vq.mirror.aliyuncs.com",
|
||||||
|
"https://registry.docker-cn.com",
|
||||||
|
"http://hub-mirror.c.163.com",
|
||||||
|
"https://docker.mirrors.ustc.edu.cn"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
3. start the container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -dit \
|
||||||
|
-v $PWD/ql/data:/ql/data \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
--restart unless-stopped \
|
||||||
|
whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker-compose Deployment
|
||||||
|
|
||||||
|
1. docker-compose installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
|
```
|
||||||
|
|
||||||
|
2. start the container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir qinglong
|
||||||
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||||
|
|
||||||
|
# start
|
||||||
|
docker-compose up -d
|
||||||
|
# stop
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
3. access
|
||||||
|
|
||||||
|
Open your browser and visit http://{ip}:5700
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
1. built-in commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update and restart Green Dragon
|
||||||
|
ql update
|
||||||
|
# Run custom scripts extra.sh
|
||||||
|
ql extra
|
||||||
|
# Adding a single script file
|
||||||
|
ql raw <file_url>
|
||||||
|
# Add a specific script for a single repository
|
||||||
|
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
||||||
|
# Delete old logs
|
||||||
|
ql rmlog <days>
|
||||||
|
# Start bot
|
||||||
|
ql bot
|
||||||
|
# Detecting the Green Dragon environment and repairing it
|
||||||
|
ql check
|
||||||
|
# Reset the number of login errors
|
||||||
|
ql resetlet
|
||||||
|
# Disable two-step login
|
||||||
|
ql resettfa
|
||||||
|
|
||||||
|
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
|
||||||
|
task <file_path>
|
||||||
|
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
|
||||||
|
# and the foreground will output the day, while recorded in the log file
|
||||||
|
task <file_path> now
|
||||||
|
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
|
||||||
|
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
|
||||||
|
task <file_path> conc <env_name> <account_number>(Optional)
|
||||||
|
# Specify the account to execute and run immediately regardless of whether a random delay is set
|
||||||
|
task <file_path> desi <env_name> <account_number>
|
||||||
|
# Set task timeout
|
||||||
|
task -m <max_time> <file_path>
|
||||||
|
# Print task log in real time, no need to carry this parameter when creating timed tasks
|
||||||
|
task -l <file_path>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
|
||||||
|
|
||||||
|
## 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/)
|
||||||
|
- [Umijs](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,6 +1,6 @@
|
|||||||
<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://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -8,12 +8,10 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
Python/JavaScript/Shell/Typescript 定时任务管理
|
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台
|
||||||
|
|
||||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![donate][donate-image]][donate-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=flat
|
|
||||||
[donate-url]: https://qinglong.whyour.cn/nice.png
|
|
||||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
||||||
@@ -24,27 +22,196 @@ Python/JavaScript/Shell/Typescript 定时任务管理
|
|||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
[](https://whyour.cn)
|
||||||
<img width="49%" src="https://qinglong.whyour.cn/login.png">
|
|
||||||
<img width="49%" src="https://qinglong.whyour.cn/home.png">
|
简体中文 | [English](./README-en.md)
|
||||||
</p>
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 支持多种脚本语言(python3、javaScript、shell、typescript)
|
||||||
|
- 支持在线管理脚本、环境变量、配置文件
|
||||||
|
- 支持在线查看任务日志
|
||||||
|
- 支持秒级任务设置
|
||||||
|
- 支持系统级通知
|
||||||
|
- 支持暗黑模式
|
||||||
|
- 支持手机端操作
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
### 本机部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 待完善,可先参考开发步骤 (windows暂时不支持)
|
||||||
|
```
|
||||||
|
|
||||||
|
### podman 部署
|
||||||
|
|
||||||
|
1. podman 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
https://podman.io/getting-started/installation
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
podman run -dit \
|
||||||
|
--network bridge \
|
||||||
|
-v $PWD/ql/data:/ql/data \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
docker.io/whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### docker 部署
|
||||||
|
|
||||||
|
1. docker 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -sSL get.docker.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 配置国内镜像源
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /etc/docker
|
||||||
|
tee /etc/docker/daemon.json <<-'EOF'
|
||||||
|
{
|
||||||
|
"registry-mirrors": [
|
||||||
|
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
||||||
|
"https://ypzju6vq.mirror.aliyuncs.com",
|
||||||
|
"https://registry.docker-cn.com",
|
||||||
|
"http://hub-mirror.c.163.com",
|
||||||
|
"https://docker.mirrors.ustc.edu.cn"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 启动容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -dit \
|
||||||
|
-v $PWD/ql/data:/ql/data \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
--restart unless-stopped \
|
||||||
|
whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### docker-compose 部署
|
||||||
|
|
||||||
|
1. docker-compose 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir qinglong
|
||||||
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
docker-compose up -d
|
||||||
|
# 停止
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 访问
|
||||||
|
|
||||||
|
打开你的浏览器,访问 http://{ip}:5700
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
1. 内置命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新并重启青龙
|
||||||
|
ql update
|
||||||
|
# 运行自定义脚本extra.sh
|
||||||
|
ql extra
|
||||||
|
# 添加单个脚本文件
|
||||||
|
ql raw <file_url>
|
||||||
|
# 添加单个仓库的指定脚本
|
||||||
|
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
||||||
|
# 删除旧日志
|
||||||
|
ql rmlog <days>
|
||||||
|
# 启动tg-bot
|
||||||
|
ql bot
|
||||||
|
# 检测青龙环境并修复
|
||||||
|
ql check
|
||||||
|
# 重置登录错误次数
|
||||||
|
ql resetlet
|
||||||
|
# 禁用两步登录
|
||||||
|
ql resettfa
|
||||||
|
|
||||||
|
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||||
|
task <file_path>
|
||||||
|
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||||
|
task <file_path> now
|
||||||
|
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
||||||
|
task <file_path> conc <env_name> <account_number>(可选的)
|
||||||
|
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
||||||
|
task <file_path> desi <env_name> <account_number>
|
||||||
|
# 设置任务超时时间
|
||||||
|
task -m <max_time> <file_path>
|
||||||
|
# 实时打印任务日志,创建定时任务时,不用携带此参数
|
||||||
|
task -l <file_path>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 参数说明
|
||||||
|
|
||||||
|
* file_url: 脚本地址
|
||||||
|
* repo_url: 仓库地址
|
||||||
|
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
|
||||||
|
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
|
||||||
|
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
|
||||||
|
* extensions: 拉取仓库的文件后缀,多个竖线分割
|
||||||
|
* branch: 拉取仓库的分支
|
||||||
|
* days: 需要保留的日志的天数
|
||||||
|
* file_path: 任务执行时的文件路径
|
||||||
|
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||||
|
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||||
|
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||||
|
|
||||||
|
## 链接
|
||||||
|
|
||||||
|
- [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/)
|
||||||
|
- [Umijs](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,129 +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 { createRandomString } from '../config/util';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
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 = createRandomString(16, 22);
|
|
||||||
fs.writeFileSync(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: authInfo.username,
|
|
||||||
password: newPassword,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return res.send({
|
|
||||||
code: 100,
|
|
||||||
message: '已初始化密码,请前往auth.json查看并重新登录',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
username == authInfo.username &&
|
|
||||||
password == authInfo.password
|
|
||||||
) {
|
|
||||||
const data = createRandomString(50, 100);
|
|
||||||
let token = jwt.sign({ data }, config.secret as any, {
|
|
||||||
expiresIn: 60 * 60 * 24 * 3,
|
|
||||||
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, message: config.authError });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.send({ err: 400, message: '请输入用户名密码!' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} 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, message: '更新成功' });
|
|
||||||
});
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+10
-7
@@ -8,10 +8,10 @@ import { celebrate, Joi } from 'celebrate';
|
|||||||
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(
|
||||||
'/configs/files',
|
'/files',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
@@ -25,18 +25,20 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/configs/:file',
|
'/:file',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let content = '';
|
let content = '';
|
||||||
|
if (config.blackFileList.includes(req.params.file)) {
|
||||||
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
|
}
|
||||||
if (req.params.file.includes('sample')) {
|
if (req.params.file.includes('sample')) {
|
||||||
content = getFileContentByName(
|
content = getFileContentByName(
|
||||||
`${config.samplePath}${req.params.file}`,
|
`${config.samplePath}${req.params.file}`,
|
||||||
@@ -48,14 +50,13 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/configs/save',
|
'/save',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
@@ -66,11 +67,13 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const { name, content } = req.body;
|
const { name, content } = req.body;
|
||||||
|
if (config.blackFileList.includes(name)) {
|
||||||
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
|
}
|
||||||
const path = `${config.configPath}${name}`;
|
const path = `${config.configPath}${name}`;
|
||||||
fs.writeFileSync(path, content);
|
fs.writeFileSync(path, content);
|
||||||
res.send({ code: 200, message: '保存成功' });
|
res.send({ code: 200, message: '保存成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+277
-44
@@ -2,21 +2,163 @@ import { Router, Request, Response, NextFunction } from 'express';
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import CronService from '../services/cron';
|
import CronService from '../services/cron';
|
||||||
|
import CronViewService from '../services/cronView';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import cron_parser from 'cron-parser';
|
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(
|
route.get(
|
||||||
'/crons',
|
'/views',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const cronViewService = Container.get(CronViewService);
|
||||||
|
const data = await cronViewService.list();
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/views',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
sorts: Joi.array().optional().allow(null),
|
||||||
|
filters: Joi.array().optional(),
|
||||||
|
filterRelation: Joi.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const cronViewService = Container.get(CronViewService);
|
||||||
|
const data = await cronViewService.create(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/views',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
id: Joi.number().required(),
|
||||||
|
sorts: Joi.array().optional().allow(null),
|
||||||
|
filters: Joi.array().optional(),
|
||||||
|
filterRelation: Joi.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const cronViewService = Container.get(CronViewService);
|
||||||
|
if (req.body.type === 1) {
|
||||||
|
return res.send({ code: 400, message: '参数错误' });
|
||||||
|
} else {
|
||||||
|
const data = await cronViewService.update(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/views',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const cronViewService = Container.get(CronViewService);
|
||||||
|
const data = await cronViewService.remove(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/views/move',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
fromIndex: Joi.number().required(),
|
||||||
|
toIndex: Joi.number().required(),
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const cronViewService = Container.get(CronViewService);
|
||||||
|
const data = await cronViewService.move(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/views/disable',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronViewService = Container.get(CronViewService);
|
||||||
|
const data = await cronViewService.disabled(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/views/enable',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronViewService = Container.get(CronViewService);
|
||||||
|
const data = await cronViewService.enabled(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.crontabs(req.query as any);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/detail',
|
||||||
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(
|
const data = await cronService.find(req.query as any);
|
||||||
req.query.searchValue as string,
|
|
||||||
);
|
|
||||||
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);
|
||||||
@@ -26,12 +168,14 @@ export default (app: Router) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
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(),
|
||||||
|
sub_id: Joi.number().optional().allow(null),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -45,16 +189,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 +206,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 +223,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 +283,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 +300,40 @@ 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),
|
||||||
|
sub_id: Joi.number().optional().allow(null),
|
||||||
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -167,16 +350,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 +367,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,40 +415,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(),
|
pid: Joi.string().optional().allow(null),
|
||||||
log_path: Joi.string().optional(),
|
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) => {
|
||||||
@@ -247,7 +462,25 @@ export default (app: Router) => {
|
|||||||
});
|
});
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/:id/logs',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.logs(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
+87
-48
@@ -3,33 +3,48 @@ import { Container } from 'typedi';
|
|||||||
import EnvService from '../services/env';
|
import EnvService from '../services/env';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import multer from 'multer';
|
||||||
|
import config from '../config';
|
||||||
|
import fs from 'fs';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.scriptPath);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, file.originalname);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/', route);
|
app.use('/envs', route);
|
||||||
route.get(
|
|
||||||
'/envs',
|
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 envService = Container.get(EnvService);
|
const data = await envService.envs(req.query.searchValue as string);
|
||||||
const data = await envService.envs(req.query.searchValue as string);
|
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(
|
||||||
'/envs',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.array().items(
|
||||||
value: Joi.string().required(),
|
Joi.object({
|
||||||
name: Joi.string().required(),
|
value: Joi.string().required(),
|
||||||
remarks: Joi.string().optional(),
|
name: Joi.string()
|
||||||
}),
|
.required()
|
||||||
|
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
|
||||||
|
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');
|
||||||
@@ -38,20 +53,19 @@ export default (app: Router) => {
|
|||||||
const data = await envService.create(req.body);
|
const data = await envService.create(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/envs',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
value: Joi.string().required(),
|
value: Joi.string().required(),
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
remarks: Joi.string().optional(),
|
remarks: Joi.string().optional().allow('').allow(null),
|
||||||
_id: Joi.string().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -61,16 +75,15 @@ export default (app: Router) => {
|
|||||||
const data = await envService.update(req.body);
|
const data = await envService.update(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/envs',
|
'/',
|
||||||
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');
|
||||||
@@ -79,40 +92,37 @@ export default (app: Router) => {
|
|||||||
const data = await envService.remove(req.body);
|
const data = await envService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/envs/: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');
|
|
||||||
try {
|
try {
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.move(req.params.id, req.body);
|
const data = await envService.move(req.params.id, req.body);
|
||||||
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(
|
||||||
'/envs/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');
|
||||||
@@ -121,16 +131,15 @@ export default (app: Router) => {
|
|||||||
const data = await envService.disabled(req.body);
|
const data = await envService.disabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/envs/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');
|
||||||
@@ -139,17 +148,16 @@ export default (app: Router) => {
|
|||||||
const data = await envService.enabled(req.body);
|
const data = await envService.enabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/envs/name',
|
'/name',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
ids: Joi.array().items(Joi.string().required()),
|
ids: Joi.array().items(Joi.number().required()),
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -160,27 +168,58 @@ export default (app: Router) => {
|
|||||||
const data = await envService.updateNames(req.body);
|
const data = await envService.updateNames(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/envs/:id',
|
'/:id',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.string().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.getDb({ id: req.params.id });
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/upload',
|
||||||
|
upload.single('env'),
|
||||||
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 envService = Container.get(EnvService);
|
||||||
const data = await envService.get(req.params.id);
|
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
|
||||||
return res.send({ code: 200, data });
|
const parseContent = JSON.parse(fileContent);
|
||||||
|
const data = Array.isArray(parseContent)
|
||||||
|
? parseContent
|
||||||
|
: [parseContent];
|
||||||
|
if (data.every((x) => x.name && x.value)) {
|
||||||
|
const result = await envService.create(
|
||||||
|
data.map((x) => ({
|
||||||
|
name: x.name,
|
||||||
|
value: x.value,
|
||||||
|
remarks: x.remarks,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return res.send({ code: 200, data: result });
|
||||||
|
} else {
|
||||||
|
return res.send({
|
||||||
|
code: 400,
|
||||||
|
message: '文件缺少name或者value字段,参考导出文件格式',
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+10
-2
@@ -1,19 +1,27 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import auth from './auth';
|
import user from './user';
|
||||||
import env from './env';
|
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 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);
|
||||||
env(app);
|
env(app);
|
||||||
config(app);
|
config(app);
|
||||||
log(app);
|
log(app);
|
||||||
cron(app);
|
cron(app);
|
||||||
script(app);
|
script(app);
|
||||||
|
open(app);
|
||||||
|
dependence(app);
|
||||||
|
system(app);
|
||||||
|
subscription(app);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
+50
-48
@@ -3,75 +3,77 @@ 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 { emptyDir, getFileContentByName, readDirs } from '../config/util';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
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);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.delete(
|
||||||
'/logs/:file',
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().allow(''),
|
||||||
|
type: Joi.string().optional()
|
||||||
|
}),
|
||||||
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const { file } = req.params;
|
let { filename, path, type } = req.body as {
|
||||||
const content = getFileContentByName(`${config.logPath}/${file}`);
|
filename: string;
|
||||||
res.send({ code: 200, data: content });
|
path: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
const filePath = join(config.logPath, path, filename);
|
||||||
|
if (type === 'directory') {
|
||||||
|
emptyDir(filePath);
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
+266
-53
@@ -1,89 +1,302 @@
|
|||||||
import { getFileContentByName, getLastModifyFilePath } from '../config/util';
|
import {
|
||||||
|
fileExist,
|
||||||
|
getFileContentByName,
|
||||||
|
readDirs,
|
||||||
|
getLastModifyFilePath,
|
||||||
|
readDir,
|
||||||
|
emptyDir,
|
||||||
|
} from '../config/util';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import path, { join, parse } from 'path';
|
||||||
|
import ScriptService from '../services/script';
|
||||||
|
import multer from 'multer';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.scriptPath);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, file.originalname);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/', route);
|
app.use('/scripts', route);
|
||||||
|
|
||||||
route.get(
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
'/scripts/files',
|
const logger: Logger = Container.get('logger');
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
try {
|
||||||
const logger: Logger = Container.get('logger');
|
let result = [];
|
||||||
try {
|
const blacklist = ['node_modules', '.git'];
|
||||||
const fileList = fs.readdirSync(config.scriptPath, 'utf-8');
|
if (req.query.path) {
|
||||||
res.send({
|
const targetPath = path.join(
|
||||||
code: 200,
|
config.scriptPath,
|
||||||
data: fileList
|
req.query.path as string,
|
||||||
.filter((x) => {
|
|
||||||
return !fs.lstatSync(config.scriptPath + x).isDirectory();
|
|
||||||
})
|
|
||||||
.map((x) => {
|
|
||||||
return { title: x, value: x, key: x };
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/scripts/:file',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const content = getFileContentByName(
|
|
||||||
`${config.scriptPath}${req.params.file}`,
|
|
||||||
);
|
);
|
||||||
|
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 });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/scripts',
|
'/',
|
||||||
|
upload.single('file'),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, path, content, originFilename, directory } =
|
||||||
|
req.body as {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
originFilename: string;
|
||||||
|
directory: 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 (directory) {
|
||||||
|
fs.mkdirSync(join(path, directory), { recursive: true });
|
||||||
|
return res.send({ code: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!originFilename) {
|
||||||
|
originFilename = filename;
|
||||||
|
}
|
||||||
|
const originFilePath = join(
|
||||||
|
path,
|
||||||
|
`${originFilename.replace(/\//g, '')}`,
|
||||||
|
);
|
||||||
|
const filePath = join(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({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().required(),
|
path: Joi.string().optional().allow(''),
|
||||||
content: Joi.string().required(),
|
content: Joi.string().required().allow(''),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path, content } = req.body as {
|
let { filename, content, path } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
|
||||||
content: string;
|
content: string;
|
||||||
|
path: string;
|
||||||
};
|
};
|
||||||
if (!path.endsWith('/')) {
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
path += '/';
|
|
||||||
}
|
|
||||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
|
||||||
return res.send({ code: 400, data: '文件路径错误,可保存目录/ql/scripts、/ql/config、/ql/jbot、/ql/bak' });
|
|
||||||
}
|
|
||||||
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
|
||||||
const bakPath = '/ql/bak';
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
if (!fs.existsSync(bakPath)) {
|
|
||||||
fs.mkdirSync(bakPath);
|
|
||||||
}
|
|
||||||
fs.copyFileSync(filePath, bakPath);
|
|
||||||
}
|
|
||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().allow(''),
|
||||||
|
type: Joi.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, path, type } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
|
if (type === 'directory') {
|
||||||
|
emptyDir(filePath);
|
||||||
|
} else {
|
||||||
|
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(),
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
pid: Joi.number().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, path, pid } = req.body;
|
||||||
|
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, pid);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/rename',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().allow(''),
|
||||||
|
newFilename: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
let { filename, path, type, newFilename } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
newFilename: string;
|
||||||
|
};
|
||||||
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
|
const newPath = join(config.scriptPath, path, newFilename);
|
||||||
|
fs.renameSync(filePath, newPath);
|
||||||
|
res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
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(),
|
||||||
|
proxy: Joi.string().optional().allow('').allow(null),
|
||||||
|
autoAddCron: Joi.boolean().optional().allow('').allow(null),
|
||||||
|
autoDelCron: Joi.boolean().optional().allow('').allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
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(),
|
||||||
|
proxy: Joi.string().optional().allow('').allow(null),
|
||||||
|
autoAddCron: Joi.boolean().optional().allow('').allow(null),
|
||||||
|
autoDelCron: Joi.boolean().optional().allow('').allow(null),
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
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,150 @@
|
|||||||
|
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';
|
||||||
|
import { parseVersion, promiseExec } from '../config/util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
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 { version, changeLog, changeLogLink } = await parseVersion(
|
||||||
|
config.versionFile,
|
||||||
|
);
|
||||||
|
const lastCommitTime = (
|
||||||
|
await promiseExec(
|
||||||
|
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
|
||||||
|
)
|
||||||
|
).replace('\n', '');
|
||||||
|
const lastCommitId = (
|
||||||
|
await promiseExec(`cd ${config.rootPath} && git rev-parse --short HEAD`)
|
||||||
|
).replace('\n', '');
|
||||||
|
const branch = (
|
||||||
|
await promiseExec(
|
||||||
|
`cd ${config.rootPath} && git symbolic-ref --short HEAD`,
|
||||||
|
)
|
||||||
|
).replace('\n', '');
|
||||||
|
|
||||||
|
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,
|
||||||
|
lastCommitTime: dayjs(lastCommitTime).unix(),
|
||||||
|
lastCommitId,
|
||||||
|
branch,
|
||||||
|
changeLog,
|
||||||
|
changeLogLink,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} 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(`✌️ 后端服务启动成功!`);
|
||||||
################################################
|
|
||||||
🛡️ 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();
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export const LOG_END_SYMBOL = ' ';
|
||||||
|
|
||||||
|
export const TASK_COMMAND = 'task';
|
||||||
|
export const QL_COMMAND = 'ql';
|
||||||
|
|
||||||
|
export const TASK_PREFIX = `${TASK_COMMAND} `;
|
||||||
|
export const QL_PREFIX = `${QL_COMMAND} `;
|
||||||
+52
-28
@@ -4,38 +4,53 @@ 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 envFile = path.join(rootPath, 'config/env.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.replace(/\/$/g, '');
|
||||||
const extraFile = path.join(rootPath, 'config/extra.sh');
|
}
|
||||||
const configPath = path.join(rootPath, 'config/');
|
|
||||||
const scriptPath = path.join(rootPath, 'scripts/');
|
const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
|
||||||
|
|
||||||
|
const rootPath = process.env.QL_DIR as string;
|
||||||
|
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||||
|
|
||||||
|
const dataPath = path.join(rootPath, 'data/');
|
||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
const logPath = path.join(rootPath, 'log/');
|
const configPath = path.join(dataPath, 'config/');
|
||||||
|
const scriptPath = path.join(dataPath, 'scripts/');
|
||||||
|
const bakPath = path.join(dataPath, 'bak/');
|
||||||
|
const logPath = path.join(dataPath, 'log/');
|
||||||
|
const dbPath = path.join(dataPath, 'db/');
|
||||||
|
const uploadPath = path.join(dataPath, 'upload/');
|
||||||
|
const sshdPath = path.join(dataPath, 'ssh.d/');
|
||||||
|
|
||||||
|
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, 'version.yaml');
|
||||||
const cronDbFile = path.join(rootPath, 'db/crontab.db');
|
|
||||||
const envDbFile = path.join(rootPath, 'db/env.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),
|
||||||
|
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
|
||||||
secret: process.env.SECRET || createRandomString(16, 32),
|
secret: process.env.SECRET || createRandomString(16, 32),
|
||||||
logs: {
|
logs: {
|
||||||
level: process.env.LOG_LEVEL || 'silly',
|
level: process.env.LOG_LEVEL || 'silly',
|
||||||
@@ -43,6 +58,7 @@ export default {
|
|||||||
api: {
|
api: {
|
||||||
prefix: '/api',
|
prefix: '/api',
|
||||||
},
|
},
|
||||||
|
rootPath,
|
||||||
configString,
|
configString,
|
||||||
loginFaild,
|
loginFaild,
|
||||||
authError,
|
authError,
|
||||||
@@ -55,8 +71,7 @@ export default {
|
|||||||
confFile,
|
confFile,
|
||||||
envFile,
|
envFile,
|
||||||
dbPath,
|
dbPath,
|
||||||
cronDbFile,
|
uploadPath,
|
||||||
envDbFile,
|
|
||||||
configPath,
|
configPath,
|
||||||
scriptPath,
|
scriptPath,
|
||||||
samplePath,
|
samplePath,
|
||||||
@@ -66,11 +81,20 @@ export default {
|
|||||||
'cookie.sh',
|
'cookie.sh',
|
||||||
'crontab.list',
|
'crontab.list',
|
||||||
'env.sh',
|
'env.sh',
|
||||||
|
'token.json',
|
||||||
],
|
],
|
||||||
writePathList: [
|
writePathList: [configPath, scriptPath],
|
||||||
'/ql/scripts/',
|
bakPath,
|
||||||
'/ql/config/',
|
apiWhiteList: [
|
||||||
'/ql/jbot/',
|
'/api/user/login',
|
||||||
'/ql/bak/',
|
'/open/auth/token',
|
||||||
|
'/api/user/two-factor/login',
|
||||||
|
'/api/system',
|
||||||
|
'/api/user/init',
|
||||||
|
'/api/user/notification/init',
|
||||||
],
|
],
|
||||||
|
versionFile,
|
||||||
|
lastVersionFile,
|
||||||
|
sqliteFile,
|
||||||
|
sshdPath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Subscription } from '../data/subscription';
|
||||||
|
import isNil from 'lodash/isNil';
|
||||||
|
|
||||||
|
export function formatUrl(doc: Subscription) {
|
||||||
|
let url = doc.url;
|
||||||
|
let host = '';
|
||||||
|
if (doc.type === 'private-repo') {
|
||||||
|
if (doc.pull_type === 'ssh-key') {
|
||||||
|
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
|
||||||
|
url = doc.url!.replace(host, doc.alias);
|
||||||
|
} else {
|
||||||
|
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
|
||||||
|
const { username, password } = doc.pull_option as any;
|
||||||
|
url = doc.url!.replace(host, `${username}:${password}@${host}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { url, host };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCommand(doc: Subscription, url?: string) {
|
||||||
|
let command = `SUB_ID=${doc.id} ql `;
|
||||||
|
let _url = url || formatUrl(doc).url;
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
whitelist,
|
||||||
|
blacklist,
|
||||||
|
dependences,
|
||||||
|
branch,
|
||||||
|
extensions,
|
||||||
|
proxy,
|
||||||
|
autoAddCron,
|
||||||
|
autoDelCron,
|
||||||
|
} = doc;
|
||||||
|
if (type === 'file') {
|
||||||
|
command += `raw "${_url}"`;
|
||||||
|
} else {
|
||||||
|
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${dependences || ''
|
||||||
|
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||||
|
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
+382
-4
@@ -1,5 +1,12 @@
|
|||||||
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';
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import psTreeFun from 'pstree.remy';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { load } from 'js-yaml';
|
||||||
|
|
||||||
export function getFileContentByName(fileName: string) {
|
export function getFileContentByName(fileName: string) {
|
||||||
if (fs.existsSync(fileName)) {
|
if (fs.existsSync(fileName)) {
|
||||||
@@ -85,7 +92,7 @@ export function createRandomString(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,7 +112,7 @@ export function createRandomString(min: number, max: number): string {
|
|||||||
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOne(arr) {
|
function getOne(arr: any[]) {
|
||||||
return arr[Math.floor(Math.random() * arr.length)];
|
return arr[Math.floor(Math.random() * arr.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +120,380 @@ export function createRandomString(min: number, max: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getToken(req: any) {
|
export function getToken(req: any) {
|
||||||
const { authorization } = req.headers;
|
const { authorization = '' } = req.headers;
|
||||||
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
||||||
return authorization.split(' ')[1];
|
return (authorization as string)
|
||||||
|
.replace('Bearer ', '')
|
||||||
|
.replace('mobile-', '')
|
||||||
|
.replace('desktop-', '');
|
||||||
}
|
}
|
||||||
return '';
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFile {
|
||||||
|
title: string;
|
||||||
|
key: string;
|
||||||
|
type: 'directory' | 'file';
|
||||||
|
parent: string;
|
||||||
|
mtime: number;
|
||||||
|
children?: IFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dirSort(a: IFile, b: IFile) {
|
||||||
|
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1;
|
||||||
|
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
parent: relativePath,
|
||||||
|
mtime: stats.mtime.getTime(),
|
||||||
|
children: readDirs(subPath, baseDir).sort(dirSort),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: file,
|
||||||
|
type: 'file',
|
||||||
|
isLeaf: true,
|
||||||
|
key,
|
||||||
|
parent: relativePath,
|
||||||
|
mtime: stats.mtime.getTime(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return result.sort(dirSort);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 emptyDir(path: string) {
|
||||||
|
const files = fs.readdirSync(path);
|
||||||
|
files.forEach((file) => {
|
||||||
|
const filePath = `${path}/${file}`;
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
emptyDir(filePath);
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fs.rmdirSync(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseHeaders(headers: string) {
|
||||||
|
if (!headers) return {};
|
||||||
|
|
||||||
|
const parsed: any = {};
|
||||||
|
let key;
|
||||||
|
let val;
|
||||||
|
let i;
|
||||||
|
|
||||||
|
headers &&
|
||||||
|
headers.split('\n').forEach(function parser(line) {
|
||||||
|
i = line.indexOf(':');
|
||||||
|
key = line.substring(0, i).trim().toLowerCase();
|
||||||
|
val = line.substring(i + 1).trim();
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBody(
|
||||||
|
body: string,
|
||||||
|
contentType:
|
||||||
|
| 'application/json'
|
||||||
|
| 'multipart/form-data'
|
||||||
|
| 'application/x-www-form-urlencoded',
|
||||||
|
) {
|
||||||
|
if (!body) return '';
|
||||||
|
|
||||||
|
const parsed: any = {};
|
||||||
|
let key;
|
||||||
|
let val;
|
||||||
|
let i;
|
||||||
|
|
||||||
|
body &&
|
||||||
|
body.split('\n').forEach(function parser(line) {
|
||||||
|
i = line.indexOf(':');
|
||||||
|
key = line.substring(0, i).trim().toLowerCase();
|
||||||
|
val = line.substring(i + 1).trim();
|
||||||
|
|
||||||
|
if (!key || parsed[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonValue = JSON.parse(val);
|
||||||
|
parsed[key] = jsonValue;
|
||||||
|
} catch (error) {
|
||||||
|
parsed[key] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (contentType) {
|
||||||
|
case 'multipart/form-data':
|
||||||
|
return Object.keys(parsed).reduce((p, c) => {
|
||||||
|
p.append(c, parsed[c]);
|
||||||
|
return p;
|
||||||
|
}, new FormData());
|
||||||
|
case 'application/x-www-form-urlencoded':
|
||||||
|
return Object.keys(parsed).reduce((p, c) => {
|
||||||
|
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function psTree(pid: number): Promise<number[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
psTreeFun(pid, (err: any, pids: number[]) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(pids.filter((x) => !isNaN(x)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function killTask(pid: number) {
|
||||||
|
const pids = await psTree(pid);
|
||||||
|
// SIGALRM 14 时钟信号
|
||||||
|
if (pids.length) {
|
||||||
|
process.kill(pids[0], 14);
|
||||||
|
} else {
|
||||||
|
process.kill(pid, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPid(name: string) {
|
||||||
|
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
let pid = (await execAsync(taskCommand)).stdout;
|
||||||
|
return Number(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IVersion {
|
||||||
|
version: string;
|
||||||
|
changeLogLink: string;
|
||||||
|
changeLog: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseVersion(path: string): Promise<IVersion> {
|
||||||
|
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseContentVersion(content: string): Promise<IVersion> {
|
||||||
|
return load(content) as IVersion;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
+48
-8
@@ -1,32 +1,44 @@
|
|||||||
|
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;
|
log_path?: string;
|
||||||
|
isPinned?: 1 | 0;
|
||||||
|
labels?: string[];
|
||||||
|
last_running_time?: number;
|
||||||
|
last_execution_time?: number;
|
||||||
|
sub_id?: 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 = CrontabStatus[options.status]
|
options.status && CrontabStatus[options.status]
|
||||||
? options.status
|
? options.status
|
||||||
: CrontabStatus.idle;
|
: 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.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;
|
||||||
|
this.sub_id = options.sub_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,3 +48,31 @@ 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,
|
||||||
|
sub_id: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { sequelize } from '.';
|
||||||
|
import { DataTypes, Model } from 'sequelize';
|
||||||
|
|
||||||
|
export enum CronViewType {
|
||||||
|
'系统' = 1,
|
||||||
|
'个人',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortType {
|
||||||
|
type: 'ASC' | 'DESC';
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterType {
|
||||||
|
property: string;
|
||||||
|
operation: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CrontabView {
|
||||||
|
name?: string;
|
||||||
|
id?: number;
|
||||||
|
position?: number;
|
||||||
|
isDisabled?: 1 | 0;
|
||||||
|
filters?: FilterType[];
|
||||||
|
sorts?: SortType[];
|
||||||
|
filterRelation?: 'and' | 'or';
|
||||||
|
type?: CronViewType;
|
||||||
|
|
||||||
|
constructor(options: CrontabView) {
|
||||||
|
this.name = options.name;
|
||||||
|
this.id = options.id;
|
||||||
|
this.position = options.position;
|
||||||
|
this.isDisabled = options.isDisabled || 0;
|
||||||
|
this.filters = options.filters;
|
||||||
|
this.sorts = options.sorts;
|
||||||
|
this.filterRelation = options.filterRelation;
|
||||||
|
this.type = options.type || CronViewType.个人;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CronViewInstance
|
||||||
|
extends Model<CrontabView, CrontabView>,
|
||||||
|
CrontabView {}
|
||||||
|
export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
||||||
|
'CrontabView',
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
unique: 'name',
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
position: DataTypes.NUMBER,
|
||||||
|
isDisabled: DataTypes.NUMBER,
|
||||||
|
filters: DataTypes.JSON,
|
||||||
|
sorts: DataTypes.JSON,
|
||||||
|
filterRelation: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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',
|
||||||
|
'queued',
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
+21
-7
@@ -1,17 +1,18 @@
|
|||||||
|
import { sequelize } from '.';
|
||||||
|
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||||
|
|
||||||
export class Env {
|
export class Env {
|
||||||
value?: string;
|
value?: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
created?: number;
|
id?: number;
|
||||||
_id?: string;
|
|
||||||
status?: EnvStatus;
|
status?: EnvStatus;
|
||||||
position?: number;
|
position?: number;
|
||||||
name?: number;
|
name?: string;
|
||||||
remarks?: number;
|
remarks?: string;
|
||||||
|
|
||||||
constructor(options: Env) {
|
constructor(options: Env) {
|
||||||
this.value = options.value;
|
this.value = options.value;
|
||||||
this._id = options._id;
|
this.id = options.id;
|
||||||
this.created = options.created || new Date().valueOf();
|
|
||||||
this.status = options.status || EnvStatus.normal;
|
this.status = options.status || EnvStatus.normal;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.position = options.position;
|
this.position = options.position;
|
||||||
@@ -25,4 +26,17 @@ export enum EnvStatus {
|
|||||||
'disabled',
|
'disabled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initEnvPosition = 9999999999;
|
export const maxPosition = 9000000000000000;
|
||||||
|
export const initPosition = 4500000000000000;
|
||||||
|
export const stepPosition = 10000000000;
|
||||||
|
export const minPosition = 100;
|
||||||
|
|
||||||
|
interface EnvInstance extends Model<Env, Env>, Env {}
|
||||||
|
export 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,22 @@
|
|||||||
|
import { Sequelize, Transaction } from 'sequelize';
|
||||||
|
import config from '../config/index';
|
||||||
|
|
||||||
|
export const sequelize = new Sequelize({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
storage: `${config.dbPath}database.sqlite`,
|
||||||
|
logging: false,
|
||||||
|
retry: {
|
||||||
|
max: 10,
|
||||||
|
match: ['SQLITE_BUSY: database is locked'],
|
||||||
|
},
|
||||||
|
pool: {
|
||||||
|
max: 5,
|
||||||
|
min: 2,
|
||||||
|
idle: 30000,
|
||||||
|
acquire: 30000,
|
||||||
|
evict: 10000,
|
||||||
|
},
|
||||||
|
transactionType: Transaction.TYPES.IMMEDIATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResponseType<T> = { code: number; data?: T; message?: string };
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { IncomingHttpHeaders } from 'http';
|
||||||
|
|
||||||
|
export enum NotificationMode {
|
||||||
|
'gotify' = 'gotify',
|
||||||
|
'goCqHttpBot' = 'goCqHttpBot',
|
||||||
|
'serverChan' = 'serverChan',
|
||||||
|
'pushDeer' = 'pushDeer',
|
||||||
|
'bark' = 'bark',
|
||||||
|
'chat' = 'chat',
|
||||||
|
'telegramBot' = 'telegramBot',
|
||||||
|
'dingtalkBot' = 'dingtalkBot',
|
||||||
|
'weWorkBot' = 'weWorkBot',
|
||||||
|
'weWorkApp' = 'weWorkApp',
|
||||||
|
'aibotk' = 'aibotk',
|
||||||
|
'iGot' = 'iGot',
|
||||||
|
'pushPlus' = 'pushPlus',
|
||||||
|
'email' = 'email',
|
||||||
|
'feishu' = 'feishu',
|
||||||
|
'webhook' = 'webhook',
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '';
|
||||||
|
public pushDeerUrl = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChatNotification extends NotificationBaseInfo {
|
||||||
|
public chatUrl = '';
|
||||||
|
public chatToken = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BarkNotification extends NotificationBaseInfo {
|
||||||
|
public barkPush = '';
|
||||||
|
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
||||||
|
public 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 AibotkNotification extends NotificationBaseInfo {
|
||||||
|
public aibotkKey: string = '';
|
||||||
|
public aibotkType: 'room' | 'contact' = 'room';
|
||||||
|
public aibotkName: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 class WebhookNotification extends NotificationBaseInfo {
|
||||||
|
public webhookHeaders: string = '';
|
||||||
|
public webhookBody: string = '';
|
||||||
|
public webhookUrl: string = '';
|
||||||
|
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
|
||||||
|
public webhookContentType:
|
||||||
|
| 'application/json'
|
||||||
|
| 'multipart/form-data'
|
||||||
|
| 'application/x-www-form-urlencoded' = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LarkNotification extends NotificationBaseInfo {
|
||||||
|
public larkKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationInfo
|
||||||
|
extends GoCqHttpBotNotification,
|
||||||
|
GotifyNotification,
|
||||||
|
ServerChanNotification,
|
||||||
|
PushDeerNotification,
|
||||||
|
ChatNotification,
|
||||||
|
BarkNotification,
|
||||||
|
TelegramBotNotification,
|
||||||
|
DingtalkBotNotification,
|
||||||
|
WeWorkBotNotification,
|
||||||
|
WeWorkAppNotification,
|
||||||
|
AibotkNotification,
|
||||||
|
IGotNotification,
|
||||||
|
PushPlusNotification,
|
||||||
|
EmailNotification,
|
||||||
|
WebhookNotification,
|
||||||
|
LarkNotification {}
|
||||||
@@ -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,115 @@
|
|||||||
|
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;
|
||||||
|
proxy?: string;
|
||||||
|
autoAddCron?: 1 | 0;
|
||||||
|
autoDelCron?: 1 | 0;
|
||||||
|
|
||||||
|
constructor(options: Subscription) {
|
||||||
|
this.id = options.id;
|
||||||
|
this.name = options.name || options.alias;
|
||||||
|
this.type = options.type;
|
||||||
|
this.schedule = options.schedule;
|
||||||
|
this.status =
|
||||||
|
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;
|
||||||
|
this.proxy = options.proxy;
|
||||||
|
this.autoAddCron = options.autoAddCron ? 1 : 0;
|
||||||
|
this.autoDelCron = options.autoDelCron ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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' },
|
||||||
|
proxy: { type: DataTypes.STRING, allowNull: true },
|
||||||
|
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
|
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
|
},
|
||||||
|
);
|
||||||
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,130 @@
|
|||||||
|
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 { fileExist } from '../config/util';
|
||||||
|
import { SubscriptionModel } from '../data/subscription';
|
||||||
|
import { CrontabViewModel } from '../data/cronView';
|
||||||
|
import config from '../config';
|
||||||
|
import { sequelize } from '../data';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
try {
|
||||||
|
await CrontabModel.sync();
|
||||||
|
await DependenceModel.sync();
|
||||||
|
await AppModel.sync();
|
||||||
|
await AuthModel.sync();
|
||||||
|
await EnvModel.sync();
|
||||||
|
await SubscriptionModel.sync();
|
||||||
|
await CrontabViewModel.sync();
|
||||||
|
|
||||||
|
// 初始化新增字段
|
||||||
|
try {
|
||||||
|
await sequelize.query(
|
||||||
|
'alter table CrontabViews add column filterRelation VARCHAR(255)',
|
||||||
|
);
|
||||||
|
} catch (error) {}
|
||||||
|
try {
|
||||||
|
await sequelize.query(
|
||||||
|
'alter table Subscriptions add column proxy VARCHAR(255)',
|
||||||
|
);
|
||||||
|
} catch (error) {}
|
||||||
|
try {
|
||||||
|
await sequelize.query('alter table CrontabViews add column type NUMBER');
|
||||||
|
} catch (error) {}
|
||||||
|
try {
|
||||||
|
await sequelize.query(
|
||||||
|
'alter table Subscriptions add column autoAddCron NUMBER',
|
||||||
|
);
|
||||||
|
} catch (error) {}
|
||||||
|
try {
|
||||||
|
await sequelize.query(
|
||||||
|
'alter table Subscriptions add column autoDelCron NUMBER',
|
||||||
|
);
|
||||||
|
} catch (error) {}
|
||||||
|
try {
|
||||||
|
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
};
|
||||||
+137
-19
@@ -1,46 +1,127 @@
|
|||||||
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, { UnauthorizedError } from 'express-jwt';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { getToken } from '../config/util';
|
import { getPlatform, getToken } from '../config/util';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import OpenService from '../services/open';
|
||||||
|
import rewrite from 'express-urlrewrite';
|
||||||
|
import 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', '/api/crons/status'],
|
secret: config.secret as string,
|
||||||
|
algorithms: ['HS384'],
|
||||||
|
}).unless({
|
||||||
|
path: [...config.apiWhiteList, /^\/open\//],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use((req, res, next) => {
|
|
||||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
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);
|
const headerToken = getToken(req);
|
||||||
if (data) {
|
if (req.path.startsWith('/open/')) {
|
||||||
const { token } = JSON.parse(data);
|
const openService = Container.get(OpenService);
|
||||||
if (token && headerToken === token) {
|
const doc = await openService.findTokenByValue(headerToken);
|
||||||
return next();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!headerToken && req.path && req.path === '/api/login') {
|
|
||||||
return next();
|
const originPath = `${req.baseUrl}${req.path === '/' ? '' : req.path}`;
|
||||||
}
|
|
||||||
const remoteAddress = req.socket.remoteAddress;
|
|
||||||
if (
|
if (
|
||||||
remoteAddress === '::ffff:127.0.0.1' &&
|
!headerToken &&
|
||||||
req.path === '/api/crons/status'
|
originPath &&
|
||||||
|
config.apiWhiteList.includes(originPath)
|
||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const err: any = new Error('UnauthorizedError');
|
|
||||||
err['status'] = 401;
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
|
if (data && headerToken) {
|
||||||
|
const { token = '', tokens = {} } = JSON.parse(data);
|
||||||
|
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
|
||||||
|
const errorMessage = headerToken
|
||||||
|
? 'jwt malformed'
|
||||||
|
: 'No authorization token was found';
|
||||||
|
const err = new UnauthorizedError(errorCode, { message: errorMessage });
|
||||||
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) => {
|
||||||
@@ -49,6 +130,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 },
|
||||||
@@ -66,6 +149,39 @@ 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({
|
||||||
|
shouldHandleError(error) {
|
||||||
|
// 排除 SequelizeUniqueConstraintError / NotFound
|
||||||
|
return (
|
||||||
|
!['SequelizeUniqueConstraintError'].includes(error.name) ||
|
||||||
|
!['Not Found'].includes(error.message)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
err: Error & { status: number },
|
err: Error & { status: number },
|
||||||
@@ -73,6 +189,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');
|
|
||||||
};
|
|
||||||
+107
-85
@@ -1,108 +1,130 @@
|
|||||||
|
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 EnvService from '../services/env';
|
import EnvService from '../services/env';
|
||||||
|
import { DependenceModel, DependenceStatus } from '../data/dependence';
|
||||||
const initData = [
|
import { Op } from 'sequelize';
|
||||||
{
|
import config from '../config';
|
||||||
name: '更新面板',
|
import { CrontabViewModel, CronViewType } from '../data/cronView';
|
||||||
command: `ql update`,
|
import { initPosition } from '../data/env';
|
||||||
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
|
|
||||||
6,
|
|
||||||
1,
|
|
||||||
).toString()} * * *`,
|
|
||||||
isDisabled: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '删除日志',
|
|
||||||
command: 'ql rmlog 7',
|
|
||||||
schedule: '30 7 */7 * *',
|
|
||||||
isDisabled: 1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const cronDb = cronService.getDb();
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
|
||||||
cronDb.count({}, async (err, count) => {
|
// 初始化新增默认全部任务视图
|
||||||
if (count === 0) {
|
CrontabViewModel.findAll({
|
||||||
const data = initData.map((x: any) => {
|
where: { type: CronViewType.系统, name: '全部任务' },
|
||||||
const tab = new Crontab(x);
|
raw: true,
|
||||||
tab.created = new Date().valueOf();
|
}).then((docs) => {
|
||||||
tab.saved = false;
|
if (docs.length === 0) {
|
||||||
if (tab.name === '更新面板') {
|
CrontabViewModel.create({
|
||||||
tab.isSystem = 1;
|
name: '全部任务',
|
||||||
} else {
|
type: CronViewType.系统,
|
||||||
tab.isSystem = 0;
|
position: initPosition / 2,
|
||||||
}
|
|
||||||
return tab;
|
|
||||||
});
|
});
|
||||||
cronDb.insert(data);
|
|
||||||
await cronService.autosave_crontab();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化更新所有任务状态为空闲
|
// 初始化更新所有任务状态为空闲
|
||||||
cronDb.update(
|
await CrontabModel.update(
|
||||||
{ status: { $in: [CrontabStatus.running, CrontabStatus.queued] } },
|
{ status: CrontabStatus.idle },
|
||||||
{ $set: { status: CrontabStatus.idle } },
|
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
|
||||||
{ multi: true },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化时执行一次所有的ql repo 任务
|
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||||
cronDb
|
DependenceModel.findAll({
|
||||||
.find({
|
where: {},
|
||||||
command: /ql (repo|raw)/,
|
order: [['type', 'DESC']],
|
||||||
isDisabled: { $ne: 1 },
|
raw: true,
|
||||||
})
|
}).then(async (docs) => {
|
||||||
.exec((err, docs) => {
|
await DependenceModel.update(
|
||||||
for (let i = 0; i < docs.length; i++) {
|
{ status: DependenceStatus.queued, log: [] },
|
||||||
const doc = docs[i];
|
{ where: { id: docs.map((x) => x.id!) } },
|
||||||
if (doc) {
|
);
|
||||||
exec(doc.command);
|
dependenceService.installDependenceOneByOne(docs);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化时执行一次所有的 ql repo 任务
|
||||||
|
CrontabModel.findAll({
|
||||||
|
where: {
|
||||||
|
isDisabled: { [Op.ne]: 1 },
|
||||||
|
command: {
|
||||||
|
[Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%ql raw%` }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).then((docs) => {
|
||||||
|
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 envService.set_envs();
|
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,74 @@
|
|||||||
|
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');
|
||||||
|
const sshdPath = path.join(dataPath, 'ssh.d');
|
||||||
|
|
||||||
|
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);
|
||||||
|
const sshdDirExist = await fileExist(sshdPath);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sshdDirExist) {
|
||||||
|
fs.mkdirSync(sshdPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
dotenv.config({ path: confFile });
|
||||||
|
|
||||||
|
Logger.info('✌️ Init file down');
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Container } from 'typedi';
|
||||||
|
import SystemService from '../services/system';
|
||||||
|
import ScheduleService from '../services/schedule';
|
||||||
|
import SubscriptionService from '../services/subscription';
|
||||||
|
import config from '../config';
|
||||||
|
import { fileExist } from '../config/util';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const scheduleService = Container.get(ScheduleService);
|
||||||
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
|
||||||
|
// 生成内置token
|
||||||
|
let tokenCommand = `tsx ${config.rootPath}/back/token.ts`;
|
||||||
|
const tokenFile = `${config.rootPath}static/build/token.js`;
|
||||||
|
if (await fileExist(tokenFile)) {
|
||||||
|
tokenCommand = `node ${tokenFile}`;
|
||||||
|
}
|
||||||
|
const cron = {
|
||||||
|
id: NaN,
|
||||||
|
name: '生成token',
|
||||||
|
command: tokenCommand,
|
||||||
|
};
|
||||||
|
await scheduleService.cancelIntervalTask(cron);
|
||||||
|
scheduleService.createIntervalTask(cron, {
|
||||||
|
days: 28,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 运行删除日志任务
|
||||||
|
const data = await systemService.getLogRemoveFrequency();
|
||||||
|
if (data && data.info && data.info.frequency) {
|
||||||
|
const rmlogCron = {
|
||||||
|
id: data.id,
|
||||||
|
name: '删除日志',
|
||||||
|
command: `ql rmlog ${data.info.frequency}`,
|
||||||
|
};
|
||||||
|
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||||
|
scheduleService.createIntervalTask(rmlogCron, {
|
||||||
|
days: data.info.frequency,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行所有订阅
|
||||||
|
await subscriptionService.setSshConfig();
|
||||||
|
const subs = await subscriptionService.list();
|
||||||
|
for (const sub of subs) {
|
||||||
|
subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Application } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import * as Tracing from '@sentry/tracing';
|
||||||
|
import Logger from './logger';
|
||||||
|
import config from '../config';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { parseVersion } from '../config/util';
|
||||||
|
|
||||||
|
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||||
|
const { version } = await parseVersion(config.versionFile);
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
||||||
|
integrations: [
|
||||||
|
new Sentry.Integrations.Http({ tracing: true }),
|
||||||
|
new Tracing.Integrations.Express({ app: expressApp }),
|
||||||
|
],
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
release: version,
|
||||||
|
});
|
||||||
|
|
||||||
|
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,22 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package com.ql.cron;
|
||||||
|
|
||||||
|
service CronService {
|
||||||
|
rpc addCron(AddCronRequest) returns (AddCronResponse);
|
||||||
|
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message Cron {
|
||||||
|
string id = 1;
|
||||||
|
string schedule = 2;
|
||||||
|
string command = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddCronRequest { repeated Cron crons = 1; }
|
||||||
|
|
||||||
|
message AddCronResponse {}
|
||||||
|
|
||||||
|
message DeleteCronRequest { repeated string ids = 1; }
|
||||||
|
|
||||||
|
message DeleteCronResponse {}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import {
|
||||||
|
CallOptions,
|
||||||
|
ChannelCredentials,
|
||||||
|
Client,
|
||||||
|
ClientOptions,
|
||||||
|
ClientUnaryCall,
|
||||||
|
handleUnaryCall,
|
||||||
|
makeGenericClientConstructor,
|
||||||
|
Metadata,
|
||||||
|
ServiceError,
|
||||||
|
UntypedServiceImplementation,
|
||||||
|
} from '@grpc/grpc-js';
|
||||||
|
import _m0 from 'protobufjs/minimal';
|
||||||
|
|
||||||
|
export const protobufPackage = 'com.ql.cron';
|
||||||
|
|
||||||
|
export interface Cron {
|
||||||
|
id: string;
|
||||||
|
schedule: string;
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddCronRequest {
|
||||||
|
crons: Cron[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddCronResponse {}
|
||||||
|
|
||||||
|
export interface DeleteCronRequest {
|
||||||
|
ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCronResponse {}
|
||||||
|
|
||||||
|
function createBaseCron(): Cron {
|
||||||
|
return { id: '', schedule: '', command: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Cron = {
|
||||||
|
encode(message: Cron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
if (message.id !== '') {
|
||||||
|
writer.uint32(10).string(message.id);
|
||||||
|
}
|
||||||
|
if (message.schedule !== '') {
|
||||||
|
writer.uint32(18).string(message.schedule);
|
||||||
|
}
|
||||||
|
if (message.command !== '') {
|
||||||
|
writer.uint32(26).string(message.command);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): Cron {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseCron();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag != 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.id = reader.string();
|
||||||
|
continue;
|
||||||
|
case 2:
|
||||||
|
if (tag != 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.schedule = reader.string();
|
||||||
|
continue;
|
||||||
|
case 3:
|
||||||
|
if (tag != 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.command = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): Cron {
|
||||||
|
return {
|
||||||
|
id: isSet(object.id) ? String(object.id) : '',
|
||||||
|
schedule: isSet(object.schedule) ? String(object.schedule) : '',
|
||||||
|
command: isSet(object.command) ? String(object.command) : '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: Cron): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
message.id !== undefined && (obj.id = message.id);
|
||||||
|
message.schedule !== undefined && (obj.schedule = message.schedule);
|
||||||
|
message.command !== undefined && (obj.command = message.command);
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<Cron>, I>>(base?: I): Cron {
|
||||||
|
return Cron.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<Cron>, I>>(object: I): Cron {
|
||||||
|
const message = createBaseCron();
|
||||||
|
message.id = object.id ?? '';
|
||||||
|
message.schedule = object.schedule ?? '';
|
||||||
|
message.command = object.command ?? '';
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseAddCronRequest(): AddCronRequest {
|
||||||
|
return { crons: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddCronRequest = {
|
||||||
|
encode(
|
||||||
|
message: AddCronRequest,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
for (const v of message.crons) {
|
||||||
|
Cron.encode(v!, writer.uint32(10).fork()).ldelim();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseAddCronRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag != 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.crons.push(Cron.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): AddCronRequest {
|
||||||
|
return {
|
||||||
|
crons: Array.isArray(object?.crons)
|
||||||
|
? object.crons.map((e: any) => Cron.fromJSON(e))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: AddCronRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.crons) {
|
||||||
|
obj.crons = message.crons.map((e) => (e ? Cron.toJSON(e) : undefined));
|
||||||
|
} else {
|
||||||
|
obj.crons = [];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): AddCronRequest {
|
||||||
|
return AddCronRequest.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(
|
||||||
|
object: I,
|
||||||
|
): AddCronRequest {
|
||||||
|
const message = createBaseAddCronRequest();
|
||||||
|
message.crons = object.crons?.map((e) => Cron.fromPartial(e)) || [];
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseAddCronResponse(): AddCronResponse {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddCronResponse = {
|
||||||
|
encode(
|
||||||
|
_: AddCronResponse,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseAddCronResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(_: any): AddCronResponse {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(_: AddCronResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): AddCronResponse {
|
||||||
|
return AddCronResponse.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(
|
||||||
|
_: I,
|
||||||
|
): AddCronResponse {
|
||||||
|
const message = createBaseAddCronResponse();
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseDeleteCronRequest(): DeleteCronRequest {
|
||||||
|
return { ids: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteCronRequest = {
|
||||||
|
encode(
|
||||||
|
message: DeleteCronRequest,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
for (const v of message.ids) {
|
||||||
|
writer.uint32(10).string(v!);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseDeleteCronRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag != 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.ids.push(reader.string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): DeleteCronRequest {
|
||||||
|
return {
|
||||||
|
ids: Array.isArray(object?.ids)
|
||||||
|
? object.ids.map((e: any) => String(e))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: DeleteCronRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.ids) {
|
||||||
|
obj.ids = message.ids.map((e) => e);
|
||||||
|
} else {
|
||||||
|
obj.ids = [];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): DeleteCronRequest {
|
||||||
|
return DeleteCronRequest.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
|
||||||
|
object: I,
|
||||||
|
): DeleteCronRequest {
|
||||||
|
const message = createBaseDeleteCronRequest();
|
||||||
|
message.ids = object.ids?.map((e) => e) || [];
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseDeleteCronResponse(): DeleteCronResponse {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteCronResponse = {
|
||||||
|
encode(
|
||||||
|
_: DeleteCronResponse,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseDeleteCronResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(_: any): DeleteCronResponse {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(_: DeleteCronResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): DeleteCronResponse {
|
||||||
|
return DeleteCronResponse.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
|
||||||
|
_: I,
|
||||||
|
): DeleteCronResponse {
|
||||||
|
const message = createBaseDeleteCronResponse();
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CronServiceService = typeof CronServiceService;
|
||||||
|
export const CronServiceService = {
|
||||||
|
addCron: {
|
||||||
|
path: '/com.ql.cron.CronService/addCron',
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: false,
|
||||||
|
requestSerialize: (value: AddCronRequest) =>
|
||||||
|
Buffer.from(AddCronRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
|
||||||
|
responseSerialize: (value: AddCronResponse) =>
|
||||||
|
Buffer.from(AddCronResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
|
||||||
|
},
|
||||||
|
delCron: {
|
||||||
|
path: '/com.ql.cron.CronService/delCron',
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: false,
|
||||||
|
requestSerialize: (value: DeleteCronRequest) =>
|
||||||
|
Buffer.from(DeleteCronRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
|
||||||
|
responseSerialize: (value: DeleteCronResponse) =>
|
||||||
|
Buffer.from(DeleteCronResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface CronServiceServer extends UntypedServiceImplementation {
|
||||||
|
addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;
|
||||||
|
delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CronServiceClient extends Client {
|
||||||
|
addCron(
|
||||||
|
request: AddCronRequest,
|
||||||
|
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
addCron(
|
||||||
|
request: AddCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
addCron(
|
||||||
|
request: AddCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
delCron(
|
||||||
|
request: DeleteCronRequest,
|
||||||
|
callback: (
|
||||||
|
error: ServiceError | null,
|
||||||
|
response: DeleteCronResponse,
|
||||||
|
) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
delCron(
|
||||||
|
request: DeleteCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (
|
||||||
|
error: ServiceError | null,
|
||||||
|
response: DeleteCronResponse,
|
||||||
|
) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
delCron(
|
||||||
|
request: DeleteCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (
|
||||||
|
error: ServiceError | null,
|
||||||
|
response: DeleteCronResponse,
|
||||||
|
) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CronServiceClient = makeGenericClientConstructor(
|
||||||
|
CronServiceService,
|
||||||
|
'com.ql.cron.CronService',
|
||||||
|
) as unknown as {
|
||||||
|
new (
|
||||||
|
address: string,
|
||||||
|
credentials: ChannelCredentials,
|
||||||
|
options?: Partial<ClientOptions>,
|
||||||
|
): CronServiceClient;
|
||||||
|
service: typeof CronServiceService;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin =
|
||||||
|
| Date
|
||||||
|
| Function
|
||||||
|
| Uint8Array
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
export type DeepPartial<T> = T extends Builtin
|
||||||
|
? T
|
||||||
|
: T extends Array<infer U>
|
||||||
|
? Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U>
|
||||||
|
? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {}
|
||||||
|
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
export type Exact<P, I extends P> = P extends Builtin
|
||||||
|
? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
|
||||||
|
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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('tail -n 300 ~/.pm2/logs/panel-error.log', (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(`✌️ 公共服务启动成功!`);
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
Logger.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import schedule from 'node-schedule';
|
|
||||||
import express from 'express';
|
|
||||||
import { exec } from 'child_process';
|
|
||||||
import Logger from './loaders/logger';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import CronService from './services/cron';
|
|
||||||
import { CrontabStatus } from './data/cron';
|
|
||||||
import config from './config';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
const run = async () => {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const cronDb = cronService.getDb();
|
|
||||||
|
|
||||||
cronDb
|
|
||||||
.find({})
|
|
||||||
.sort({ created: 1 })
|
|
||||||
.exec((err, docs) => {
|
|
||||||
if (err) {
|
|
||||||
Logger.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (docs && docs.length > 0) {
|
|
||||||
for (let i = 0; i < docs.length; i++) {
|
|
||||||
const task = docs[i];
|
|
||||||
const _schedule = task.schedule && task.schedule.split(' ');
|
|
||||||
if (
|
|
||||||
_schedule &&
|
|
||||||
_schedule.length > 5 &&
|
|
||||||
task.status !== CrontabStatus.disabled &&
|
|
||||||
!task.isDisabled
|
|
||||||
) {
|
|
||||||
schedule.scheduleJob(task.schedule, function () {
|
|
||||||
let command = task.command as string;
|
|
||||||
if (!command.includes('task ') && !command.includes('ql ')) {
|
|
||||||
command = `task ${command}`;
|
|
||||||
}
|
|
||||||
exec(command);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
app
|
|
||||||
.listen(config.cronPort, () => {
|
|
||||||
run();
|
|
||||||
Logger.info(`
|
|
||||||
################################################
|
|
||||||
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
|
||||||
################################################
|
|
||||||
`);
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
Logger.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { AddCronRequest, AddCronResponse } from '../protos/cron';
|
||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { scheduleStacks } from './data';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
|
||||||
|
const addCron = (
|
||||||
|
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
|
||||||
|
callback: sendUnaryData<AddCronResponse>,
|
||||||
|
) => {
|
||||||
|
for (const item of call.request.crons) {
|
||||||
|
const { id, schedule, command } = item;
|
||||||
|
if (scheduleStacks.has(id)) {
|
||||||
|
scheduleStacks.get(id)?.cancel();
|
||||||
|
}
|
||||||
|
scheduleStacks.set(
|
||||||
|
id,
|
||||||
|
nodeSchedule.scheduleJob(id, schedule, async () => {
|
||||||
|
exec(`ID=${id} ${command}`);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { addCron };
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { credentials } from '@grpc/grpc-js';
|
||||||
|
import {
|
||||||
|
AddCronRequest,
|
||||||
|
AddCronResponse,
|
||||||
|
CronServiceClient,
|
||||||
|
DeleteCronRequest,
|
||||||
|
DeleteCronResponse,
|
||||||
|
} from '../protos/cron';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
private client = new CronServiceClient(
|
||||||
|
`localhost:${config.cronPort}`,
|
||||||
|
credentials.createInsecure(),
|
||||||
|
);
|
||||||
|
|
||||||
|
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.addCron({ crons: request }, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.delCron({ ids: request }, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Client();
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { ToadScheduler } from 'toad-scheduler';
|
||||||
|
|
||||||
|
export const scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||||
|
|
||||||
|
export const intervalSchedule = new ToadScheduler();
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
|
||||||
|
import { scheduleStacks } from './data';
|
||||||
|
|
||||||
|
const delCron = (
|
||||||
|
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
|
||||||
|
callback: sendUnaryData<DeleteCronResponse>,
|
||||||
|
) => {
|
||||||
|
for (const id of call.request.ids) {
|
||||||
|
if (scheduleStacks.has(id)) {
|
||||||
|
scheduleStacks.get(id)?.cancel();
|
||||||
|
scheduleStacks.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { delCron };
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Server, ServerCredentials } from '@grpc/grpc-js';
|
||||||
|
import { CronServiceService } from '../protos/cron';
|
||||||
|
import { addCron } from './addCron';
|
||||||
|
import { delCron } from './delCron';
|
||||||
|
import config from '../config';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
const server = new Server();
|
||||||
|
server.addService(CronServiceService, { addCron, delCron });
|
||||||
|
server.bindAsync(
|
||||||
|
`localhost:${config.cronPort}`,
|
||||||
|
ServerCredentials.createInsecure(),
|
||||||
|
() => {
|
||||||
|
server.start();
|
||||||
|
Logger.debug(`✌️ 定时服务启动成功!`);
|
||||||
|
},
|
||||||
|
);
|
||||||
+456
-266
@@ -1,37 +1,29 @@
|
|||||||
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 {
|
||||||
import PQueue from 'p-queue';
|
getFileContentByName,
|
||||||
|
concurrentRun,
|
||||||
|
fileExist,
|
||||||
|
killTask,
|
||||||
|
} from '../config/util';
|
||||||
import { promises, existsSync } from 'fs';
|
import { promises, existsSync } from 'fs';
|
||||||
import { promisify } from 'util';
|
import { Op, where, col as colFn, FindOptions } from 'sequelize';
|
||||||
|
import path from 'path';
|
||||||
|
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
||||||
|
import cronClient from '../schedule/client';
|
||||||
|
|
||||||
@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;
|
||||||
@@ -39,50 +31,48 @@ 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));
|
if (this.isSixCron(doc)) {
|
||||||
|
await cronClient.addCron([
|
||||||
|
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await this.set_crontab();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
const doc = await this.getDb({ id: payload.id });
|
||||||
const doc = await this.get(_id);
|
const tab = new Crontab({ ...doc, ...payload });
|
||||||
const tab = new Crontab({ ...doc, ...other });
|
|
||||||
tab.saved = false;
|
tab.saved = false;
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
await this.set_crontab(this.isSixCron(newDoc));
|
if (doc.isDisabled === 1) {
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
if (this.isSixCron(doc)) {
|
||||||
|
await cronClient.delCron([String(newDoc.id)]);
|
||||||
|
}
|
||||||
|
if (this.isSixCron(newDoc)) {
|
||||||
|
await cronClient.addCron([
|
||||||
|
{
|
||||||
|
id: String(newDoc.id),
|
||||||
|
schedule: newDoc.schedule!,
|
||||||
|
command: newDoc.command,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await this.set_crontab();
|
||||||
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({
|
||||||
@@ -90,252 +80,451 @@ export default class CronService {
|
|||||||
status,
|
status,
|
||||||
pid,
|
pid,
|
||||||
log_path,
|
log_path,
|
||||||
|
last_running_time = 0,
|
||||||
|
last_execution_time = 0,
|
||||||
}: {
|
}: {
|
||||||
ids: string[];
|
ids: number[];
|
||||||
status: CrontabStatus;
|
status: CrontabStatus;
|
||||||
pid: number;
|
pid: number;
|
||||||
log_path: string;
|
log_path: string;
|
||||||
|
last_running_time: number;
|
||||||
|
last_execution_time: number;
|
||||||
}) {
|
}) {
|
||||||
this.cronDb.update(
|
const options: any = {
|
||||||
{ _id: { $in: ids } },
|
status,
|
||||||
{ $set: { status, pid, log_path } },
|
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 cronClient.delCron(ids.map(String));
|
||||||
{ _id: { $in: ids } },
|
await this.set_crontab();
|
||||||
{ multi: true },
|
}
|
||||||
async (err) => {
|
|
||||||
await this.set_crontab(true);
|
public async pin(ids: number[]) {
|
||||||
resolve();
|
await CrontabModel.update({ isPinned: 1 }, { where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 crontabs(searchText?: string): Promise<Crontab[]> {
|
public async removeLabels(ids: string[], labels: string[]) {
|
||||||
let query = {};
|
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 } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatViewQuery(query: any, viewQuery: any) {
|
||||||
|
if (viewQuery.filters && viewQuery.filters.length > 0) {
|
||||||
|
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
|
||||||
|
if (!query[primaryOperate]) {
|
||||||
|
query[primaryOperate] = [];
|
||||||
|
}
|
||||||
|
for (const col of viewQuery.filters) {
|
||||||
|
const { property, value, operation } = col;
|
||||||
|
let q: any = {};
|
||||||
|
let operate2 = null;
|
||||||
|
let operate = null;
|
||||||
|
switch (operation) {
|
||||||
|
case 'Reg':
|
||||||
|
operate = Op.like;
|
||||||
|
operate2 = Op.or;
|
||||||
|
break;
|
||||||
|
case 'NotReg':
|
||||||
|
operate = Op.notLike;
|
||||||
|
operate2 = Op.and;
|
||||||
|
break;
|
||||||
|
case 'In':
|
||||||
|
q[Op.or] = [
|
||||||
|
{
|
||||||
|
[property]: value,
|
||||||
|
},
|
||||||
|
property === 'status' && value.includes(2)
|
||||||
|
? { isDisabled: 1 }
|
||||||
|
: {},
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'Nin':
|
||||||
|
q[Op.and] = [
|
||||||
|
{
|
||||||
|
[property]: {
|
||||||
|
[Op.notIn]: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
property === 'status' && value.includes(2)
|
||||||
|
? { isDisabled: { [Op.ne]: 1 } }
|
||||||
|
: {},
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (operate && operate2) {
|
||||||
|
q[property] = {
|
||||||
|
[Op.or]: [
|
||||||
|
{
|
||||||
|
[operate2]: [
|
||||||
|
{ [operate]: `%${value}%` },
|
||||||
|
{ [operate]: `%${encodeURIComponent(value)}%` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[operate2]: [
|
||||||
|
where(colFn(property), operate, `%${value}%`),
|
||||||
|
where(
|
||||||
|
colFn(property),
|
||||||
|
operate,
|
||||||
|
`%${encodeURIComponent(value)}%`,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
query[primaryOperate].push(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatSearchText(query: any, searchText: string | undefined) {
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const reg = new RegExp(searchText, 'i');
|
if (!query[Op.and]) {
|
||||||
query = {
|
query[Op.and] = [];
|
||||||
$or: [
|
}
|
||||||
{
|
let q: any = {};
|
||||||
name: reg,
|
const textArray = searchText.split(':');
|
||||||
},
|
switch (textArray[0]) {
|
||||||
{
|
case 'name':
|
||||||
command: reg,
|
case 'command':
|
||||||
},
|
case 'schedule':
|
||||||
{
|
case 'label':
|
||||||
schedule: reg,
|
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
|
||||||
},
|
q[column] = {
|
||||||
],
|
[Op.or]: [
|
||||||
};
|
{ [Op.like]: `%${textArray[1]}%` },
|
||||||
}
|
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
||||||
return new Promise((resolve) => {
|
],
|
||||||
this.cronDb
|
};
|
||||||
.find(query)
|
break;
|
||||||
.sort({ created: -1 })
|
default:
|
||||||
.exec((err, docs) => {
|
const reg = {
|
||||||
resolve(docs);
|
[Op.or]: [
|
||||||
});
|
{ [Op.like]: `%${searchText}%` },
|
||||||
});
|
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||||
}
|
],
|
||||||
|
};
|
||||||
public async get(_id: string): Promise<Crontab> {
|
q[Op.or] = [
|
||||||
return new Promise((resolve) => {
|
{
|
||||||
this.cronDb.find({ _id }).exec((err, docs) => {
|
name: reg,
|
||||||
resolve(docs[0]);
|
},
|
||||||
});
|
{
|
||||||
});
|
command: reg,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
public async run(ids: string[]) {
|
schedule: reg,
|
||||||
this.cronDb.update(
|
},
|
||||||
{ _id: { $in: ids } },
|
{
|
||||||
{ $set: { status: CrontabStatus.queued } },
|
labels: reg,
|
||||||
{ multi: true },
|
},
|
||||||
);
|
];
|
||||||
for (let i = 0; i < ids.length; i++) {
|
break;
|
||||||
const id = ids[i];
|
}
|
||||||
this.queue.add(() => this.runSingle(id));
|
query[Op.and].push(q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(ids: string[]) {
|
private formatFilterQuery(query: any, filterQuery: any) {
|
||||||
return new Promise((resolve: any) => {
|
if (filterQuery) {
|
||||||
this.cronDb
|
if (!query[Op.and]) {
|
||||||
.find({ _id: { $in: ids } })
|
query[Op.and] = [];
|
||||||
.exec(async (err, docs: Crontab[]) => {
|
}
|
||||||
for (const doc of docs) {
|
const filterKeys: any = Object.keys(filterQuery);
|
||||||
if (doc.pid) {
|
for (const key of filterKeys) {
|
||||||
try {
|
let q: any = {};
|
||||||
process.kill(-doc.pid);
|
if (!filterQuery[key]) continue;
|
||||||
} catch (error) {
|
if (key === 'status') {
|
||||||
this.logger.silly(error);
|
if (filterQuery[key].includes(2)) {
|
||||||
}
|
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
|
||||||
}
|
} else {
|
||||||
const err = await this.killTask(doc.command);
|
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
|
||||||
if (doc.log_path) {
|
|
||||||
const str = err ? `\n${err}` : '';
|
|
||||||
fs.appendFileSync(
|
|
||||||
`${doc.log_path}`,
|
|
||||||
`${str}\n## 执行结束... ${new Date().toLocaleString()} `,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.cronDb.update(
|
} else {
|
||||||
{ _id: { $in: ids } },
|
q[key] = filterQuery[key];
|
||||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
|
||||||
{ multi: true },
|
|
||||||
);
|
|
||||||
this.queue.clear();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async killTask(name: string) {
|
|
||||||
let taskCommond = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
try {
|
|
||||||
let pid = (await execAsync(taskCommond)).stdout;
|
|
||||||
if (pid) {
|
|
||||||
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pids = pid.match(/\d+/g);
|
|
||||||
const killLogs = [];
|
|
||||||
for (const id of pids) {
|
|
||||||
const c = `kill -9 ${id}`;
|
|
||||||
const { stdout, stderr } = await execAsync(c);
|
|
||||||
if (stderr) {
|
|
||||||
killLogs.push(stderr);
|
|
||||||
}
|
|
||||||
if (stdout) {
|
|
||||||
killLogs.push(stdout);
|
|
||||||
}
|
}
|
||||||
|
query[Op.and].push(q);
|
||||||
}
|
}
|
||||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
|
||||||
} catch (e) {
|
|
||||||
return JSON.stringify(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runSingle(id: string): Promise<number> {
|
private formatViewSort(order: string[][], viewQuery: any) {
|
||||||
|
if (viewQuery.sorts && viewQuery.sorts.length > 0) {
|
||||||
|
for (const { property, type } of viewQuery.sorts) {
|
||||||
|
order.unshift([property, type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async find({
|
||||||
|
log_path,
|
||||||
|
}: {
|
||||||
|
log_path: string;
|
||||||
|
}): Promise<Crontab | null> {
|
||||||
|
try {
|
||||||
|
const result = await CrontabModel.findOne({ where: { log_path } });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async crontabs(params?: {
|
||||||
|
searchValue: string;
|
||||||
|
page: string;
|
||||||
|
size: string;
|
||||||
|
sorter: string;
|
||||||
|
filters: string;
|
||||||
|
queryString: string;
|
||||||
|
}): Promise<{ data: Crontab[]; total: number }> {
|
||||||
|
const searchText = params?.searchValue;
|
||||||
|
const page = Number(params?.page || '0');
|
||||||
|
const size = Number(params?.size || '0');
|
||||||
|
const viewQuery = JSON.parse(params?.queryString || '{}');
|
||||||
|
const filterQuery = JSON.parse(params?.filters || '{}');
|
||||||
|
const sorterQuery = JSON.parse(params?.sorter || '{}');
|
||||||
|
|
||||||
|
let query: any = {};
|
||||||
|
let order = [
|
||||||
|
['isPinned', 'DESC'],
|
||||||
|
['isDisabled', 'ASC'],
|
||||||
|
['status', 'ASC'],
|
||||||
|
['createdAt', 'DESC'],
|
||||||
|
];
|
||||||
|
|
||||||
|
this.formatViewQuery(query, viewQuery);
|
||||||
|
this.formatSearchText(query, searchText);
|
||||||
|
this.formatFilterQuery(query, filterQuery);
|
||||||
|
this.formatViewSort(order, viewQuery);
|
||||||
|
|
||||||
|
if (sorterQuery) {
|
||||||
|
const { field, type } = sorterQuery;
|
||||||
|
if (field && type) {
|
||||||
|
order.unshift([field, type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let condition: any = {
|
||||||
|
where: query,
|
||||||
|
order: order,
|
||||||
|
};
|
||||||
|
if (page && size) {
|
||||||
|
condition.offset = (page - 1) * size;
|
||||||
|
condition.limit = size;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await CrontabModel.findAll(condition);
|
||||||
|
const count = await CrontabModel.count({ where: query });
|
||||||
|
return { data: result, total: count };
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {
|
||||||
|
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
||||||
|
return doc && (doc.get({ plain: true }) as Crontab);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run(ids: number[]) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.queued },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
concurrentRun(
|
||||||
|
ids.map((id) => async () => await 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 {
|
||||||
|
await killTask(doc.pid);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.silly(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runSingle(cronId: number): 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 cmdStr = command;
|
let cmdStr = command;
|
||||||
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
|
||||||
cmdStr = `task ${cmdStr}`;
|
cmdStr = `${TASK_PREFIX}${cmdStr}`;
|
||||||
}
|
}
|
||||||
if (cmdStr.endsWith('.js')) {
|
if (
|
||||||
|
cmdStr.endsWith('.js') ||
|
||||||
|
cmdStr.endsWith('.py') ||
|
||||||
|
cmdStr.endsWith('.pyc') ||
|
||||||
|
cmdStr.endsWith('.sh') ||
|
||||||
|
cmdStr.endsWith('.ts')
|
||||||
|
) {
|
||||||
cmdStr = `${cmdStr} now`;
|
cmdStr = `${cmdStr} now`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cp = spawn(cmdStr, { shell: '/bin/bash', detached: true });
|
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
|
||||||
this.cronDb.update(
|
|
||||||
{ _id },
|
await CrontabModel.update(
|
||||||
{ $set: { status: CrontabStatus.running, pid: cp.pid } },
|
{ status: CrontabStatus.running, pid: cp.pid },
|
||||||
|
{ where: { id } },
|
||||||
);
|
);
|
||||||
cp.on('close', (code) => {
|
cp.stderr.on('data', (data) => {
|
||||||
this.cronDb.update(
|
if (logFileExist) {
|
||||||
{ _id },
|
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
|
||||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
}
|
||||||
|
});
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
if (logFileExist) {
|
||||||
|
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code, signal) => {
|
||||||
|
this.logger.info(
|
||||||
|
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cp.on('close', async (code) => {
|
||||||
|
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 cronClient.delCron(ids.map(String));
|
||||||
{ _id: { $in: ids } },
|
await this.set_crontab();
|
||||||
{ $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(
|
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||||
{ _id: { $in: ids } },
|
const sixCron = docs
|
||||||
{ $set: { isDisabled: 0 } },
|
.filter((x) => this.isSixCron(x))
|
||||||
{ multi: true },
|
.map((doc) => ({
|
||||||
async (err) => {
|
id: String(doc.id),
|
||||||
await this.set_crontab(true);
|
schedule: doc.schedule!,
|
||||||
resolve();
|
command: doc.command,
|
||||||
},
|
}));
|
||||||
);
|
await cronClient.addCron(sixCron);
|
||||||
});
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(_id: string) {
|
public async log(id: number) {
|
||||||
const doc = await this.get(_id);
|
const doc = await this.getDb({ id });
|
||||||
if (doc.log_path) {
|
if (!doc) {
|
||||||
return getFileContentByName(`${doc.log_path}`);
|
|
||||||
}
|
|
||||||
const [, commandStr, url] = doc.command.split(' ');
|
|
||||||
let logPath = this.getKey(commandStr);
|
|
||||||
const isQlCommand = doc.command.startsWith('ql ');
|
|
||||||
const key =
|
|
||||||
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
|
|
||||||
logPath;
|
|
||||||
if (isQlCommand) {
|
|
||||||
logPath = 'update';
|
|
||||||
}
|
|
||||||
let logDir = `${config.logPath}${logPath}`;
|
|
||||||
if (existsSync(logDir)) {
|
|
||||||
let files = await promises.readdir(logDir);
|
|
||||||
if (isQlCommand) {
|
|
||||||
files = files.filter((x) => x.includes(key));
|
|
||||||
}
|
|
||||||
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
|
|
||||||
} else {
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||||
|
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||||
|
if (logFileExist) {
|
||||||
|
return getFileContentByName(`${absolutePath}`);
|
||||||
|
} else {
|
||||||
|
return '任务未运行';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getKey(command: string) {
|
public async logs(id: number) {
|
||||||
const start =
|
const doc = await this.getDb({ id });
|
||||||
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
if (!doc || !doc.log_path) {
|
||||||
const end =
|
return [];
|
||||||
command.lastIndexOf('.') !== -1
|
}
|
||||||
? command.lastIndexOf('.')
|
|
||||||
: command.length;
|
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||||
return command.substring(start, end);
|
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);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private make_command(tab: Crontab) {
|
private make_command(tab: Crontab) {
|
||||||
const crontab_job_string = `ID=${tab._id} ${tab.command}`;
|
if (
|
||||||
|
!tab.command.startsWith(TASK_PREFIX) &&
|
||||||
|
!tab.command.startsWith(QL_PREFIX)
|
||||||
|
) {
|
||||||
|
tab.command = `${TASK_PREFIX}${tab.command}`;
|
||||||
|
}
|
||||||
|
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
||||||
return crontab_job_string;
|
return crontab_job_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async set_crontab(needReloadSchedule: boolean = false) {
|
private async set_crontab(data?: { data: Crontab[]; total: number }) {
|
||||||
const tabs = await this.crontabs();
|
const tabs = data ?? (await this.crontabs());
|
||||||
var crontab_string = '';
|
var crontab_string = '';
|
||||||
tabs.forEach((tab) => {
|
tabs.data.forEach((tab) => {
|
||||||
const _schedule = tab.schedule && tab.schedule.split(' ');
|
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||||
if (tab.isDisabled === 1 || _schedule.length !== 5) {
|
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
|
||||||
crontab_string += '# ';
|
crontab_string += '# ';
|
||||||
crontab_string += tab.schedule;
|
crontab_string += tab.schedule;
|
||||||
crontab_string += ' ';
|
crontab_string += ' ';
|
||||||
@@ -353,53 +542,54 @@ export default class CronService {
|
|||||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||||
|
|
||||||
execSync(`crontab ${config.crontabFile}`);
|
execSync(`crontab ${config.crontabFile}`);
|
||||||
if (needReloadSchedule) {
|
await CrontabModel.update({ saved: true }, { where: {} });
|
||||||
exec(`pm2 reload schedule`);
|
|
||||||
}
|
|
||||||
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public autosave_crontab() {
|
public async autosave_crontab() {
|
||||||
return this.set_crontab();
|
const tabs = await this.crontabs();
|
||||||
|
this.set_crontab(tabs);
|
||||||
|
|
||||||
|
const sixCron = tabs.data
|
||||||
|
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
|
||||||
|
.map((doc) => ({
|
||||||
|
id: String(doc.id),
|
||||||
|
schedule: doc.schedule!,
|
||||||
|
command: doc.command,
|
||||||
|
}));
|
||||||
|
await cronClient.addCron(sixCron);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
||||||
|
import {
|
||||||
|
initPosition,
|
||||||
|
maxPosition,
|
||||||
|
minPosition,
|
||||||
|
stepPosition,
|
||||||
|
} from '../data/env';
|
||||||
|
import { FindOptions } from 'sequelize';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class CronViewService {
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public async create(payload: CrontabView): Promise<CrontabView> {
|
||||||
|
let position = initPosition;
|
||||||
|
const views = await this.list();
|
||||||
|
if (views && views.length > 0 && views[views.length - 1].position) {
|
||||||
|
position = views[views.length - 1].position as number;
|
||||||
|
}
|
||||||
|
position = position / 2;
|
||||||
|
const tab = new CrontabView({ ...payload, position });
|
||||||
|
const doc = await this.insert(tab);
|
||||||
|
|
||||||
|
await this.checkPosition(tab.position!);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insert(payload: CrontabView): Promise<CrontabView> {
|
||||||
|
return await CrontabViewModel.create(payload, { returning: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(payload: CrontabView): Promise<CrontabView> {
|
||||||
|
const doc = await this.getDb({ id: payload.id })
|
||||||
|
const tab = new CrontabView({ ...doc, ...payload });
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateDb(payload: CrontabView): Promise<CrontabView> {
|
||||||
|
await CrontabViewModel.update(payload, { where: { id: payload.id } });
|
||||||
|
return await this.getDb({ id: payload.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(ids: number[]) {
|
||||||
|
await CrontabViewModel.destroy({ where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(): Promise<CrontabView[]> {
|
||||||
|
try {
|
||||||
|
const result = await CrontabViewModel.findAll({
|
||||||
|
where: {},
|
||||||
|
order: [['position', 'DESC']],
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(query: FindOptions<CrontabView>['where']): Promise<CrontabView> {
|
||||||
|
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
|
||||||
|
return doc && (doc.get({ plain: true }) as CrontabView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disabled(ids: number[]) {
|
||||||
|
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enabled(ids: number[]) {
|
||||||
|
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkPosition(position: number) {
|
||||||
|
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||||
|
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
|
||||||
|
const envs = await this.list();
|
||||||
|
let position = initPosition;
|
||||||
|
for (const env of envs) {
|
||||||
|
position = position - stepPosition;
|
||||||
|
await this.updateDb({ id: env.id, position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPrecisionPosition(position: number): number {
|
||||||
|
return parseFloat(position.toPrecision(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async move({
|
||||||
|
id,
|
||||||
|
fromIndex,
|
||||||
|
toIndex,
|
||||||
|
}: {
|
||||||
|
fromIndex: number;
|
||||||
|
toIndex: number;
|
||||||
|
id: number;
|
||||||
|
}): Promise<CrontabView> {
|
||||||
|
let targetPosition: number;
|
||||||
|
const isUpward = fromIndex > toIndex;
|
||||||
|
const views = await this.list();
|
||||||
|
if (toIndex === 0 || toIndex === views.length - 1) {
|
||||||
|
targetPosition = isUpward
|
||||||
|
? views[0].position! * 2
|
||||||
|
: views[toIndex].position! / 2;
|
||||||
|
} else {
|
||||||
|
targetPosition = isUpward
|
||||||
|
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
|
||||||
|
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
|
||||||
|
}
|
||||||
|
const newDoc = await this.update({
|
||||||
|
id,
|
||||||
|
position: this.getPrecisionPosition(targetPosition),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.checkPosition(targetPosition);
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import config from '../config';
|
||||||
|
import {
|
||||||
|
Dependence,
|
||||||
|
InstallDependenceCommandTypes,
|
||||||
|
DependenceStatus,
|
||||||
|
DependenceTypes,
|
||||||
|
unInstallDependenceCommandTypes,
|
||||||
|
DependenceModel,
|
||||||
|
} from '../data/dependence';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import SockService from './sock';
|
||||||
|
import { FindOptions, 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.queued });
|
||||||
|
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.queued,
|
||||||
|
});
|
||||||
|
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.queued, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public installDependenceOneByOne(
|
||||||
|
docs: Dependence[],
|
||||||
|
isInstall: boolean = true,
|
||||||
|
force: boolean = false,
|
||||||
|
) {
|
||||||
|
concurrentRun(
|
||||||
|
docs.map((dep) => async () => {
|
||||||
|
const status = isInstall
|
||||||
|
? DependenceStatus.installing
|
||||||
|
: DependenceStatus.removing;
|
||||||
|
await DependenceModel.update({ status }, { where: { id: dep.id } });
|
||||||
|
return await this.installOrUninstallDependencies(
|
||||||
|
[dep],
|
||||||
|
isInstall,
|
||||||
|
force,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.queued, 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 },
|
||||||
|
order: [...sort, ['createdAt', 'DESC']],
|
||||||
|
});
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(
|
||||||
|
query: FindOptions<Dependence>['where'],
|
||||||
|
): 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+132
-146
@@ -1,86 +1,73 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { getFileContentByName } from '../config/util';
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import DataStore from 'nedb';
|
import {
|
||||||
import { Env, EnvStatus, initEnvPosition } from '../data/env';
|
Env,
|
||||||
import _ from 'lodash';
|
EnvModel,
|
||||||
|
EnvStatus,
|
||||||
|
initPosition,
|
||||||
|
maxPosition,
|
||||||
|
minPosition,
|
||||||
|
stepPosition,
|
||||||
|
} from '../data/env';
|
||||||
|
import groupBy from 'lodash/groupBy';
|
||||||
|
import { FindOptions, Op } from 'sequelize';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class EnvService {
|
export default class EnvService {
|
||||||
private cronDb = new DataStore({ filename: config.envDbFile });
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
|
||||||
this.cronDb.loadDatabase((err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async create(payload: Env): Promise<Env> {
|
public async create(payloads: Env[]): Promise<Env[]> {
|
||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
let position = initEnvPosition;
|
let position = initPosition;
|
||||||
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
|
if (
|
||||||
position = envs[envs.length - 1].position;
|
envs &&
|
||||||
|
envs.length > 0 &&
|
||||||
|
typeof envs[envs.length - 1].position === 'number'
|
||||||
|
) {
|
||||||
|
position = envs[envs.length - 1].position!;
|
||||||
}
|
}
|
||||||
const tab = new Env({ ...payload, position: position / 2 });
|
const tabs = payloads.map((x) => {
|
||||||
const doc = await this.insert(tab);
|
position = position - stepPosition;
|
||||||
|
const tab = new Env({ ...x, position });
|
||||||
|
return tab;
|
||||||
|
});
|
||||||
|
const docs = await this.insert(tabs);
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
return doc;
|
await this.checkPosition(tabs[tabs.length - 1].position!);
|
||||||
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payload: Env): Promise<Env> {
|
public async insert(payloads: Env[]): Promise<Env[]> {
|
||||||
return new Promise((resolve) => {
|
const result = [];
|
||||||
this.cronDb.insert(payload, (err, doc) => {
|
for (const env of payloads) {
|
||||||
if (err) {
|
const doc = await EnvModel.create(env, { returning: true });
|
||||||
this.logger.error(err);
|
result.push(doc);
|
||||||
} else {
|
}
|
||||||
resolve(doc);
|
return result;
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Env): Promise<Env> {
|
public async update(payload: Env): Promise<Env> {
|
||||||
const { _id, ...other } = payload;
|
const doc = await this.getDb({ id: payload.id })
|
||||||
const doc = await this.get(_id);
|
const tab = new Env({ ...doc, ...payload });
|
||||||
const tab = new Env({ ...doc, ...other });
|
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateDb(payload: Env): Promise<Env> {
|
private async updateDb(payload: Env): Promise<Env> {
|
||||||
return new Promise((resolve) => {
|
await EnvModel.update({ ...payload }, { where: { id: payload.id } });
|
||||||
this.cronDb.update(
|
return await this.getDb({ id: payload.id });
|
||||||
{ _id: payload._id },
|
|
||||||
payload,
|
|
||||||
{ returnUpdatedDocs: true },
|
|
||||||
(err, num, doc) => {
|
|
||||||
if (err) {
|
|
||||||
this.logger.error(err);
|
|
||||||
} else {
|
|
||||||
resolve(doc as Env);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: string[]) {
|
public async remove(ids: string[]) {
|
||||||
return new Promise((resolve: any) => {
|
await EnvModel.destroy({ where: { id: ids } });
|
||||||
this.cronDb.remove(
|
await this.set_envs();
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ multi: true },
|
|
||||||
async (err) => {
|
|
||||||
await this.set_envs();
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async move(
|
public async move(
|
||||||
_id: string,
|
id: number,
|
||||||
{
|
{
|
||||||
fromIndex,
|
fromIndex,
|
||||||
toIndex,
|
toIndex,
|
||||||
@@ -88,141 +75,140 @@ export default class EnvService {
|
|||||||
fromIndex: number;
|
fromIndex: number;
|
||||||
toIndex: number;
|
toIndex: number;
|
||||||
},
|
},
|
||||||
) {
|
): Promise<Env> {
|
||||||
let targetPosition: number;
|
let targetPosition: number;
|
||||||
const isUpward = fromIndex > toIndex;
|
const isUpward = fromIndex > toIndex;
|
||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
if (toIndex === 0 || toIndex === envs.length - 1) {
|
if (toIndex === 0 || toIndex === envs.length - 1) {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? envs[0].position * 2
|
? envs[0].position! + stepPosition
|
||||||
: envs[toIndex].position / 2;
|
: envs[toIndex].position! - stepPosition;
|
||||||
} else {
|
} else {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
|
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
|
||||||
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
|
||||||
}
|
}
|
||||||
this.update({
|
|
||||||
_id,
|
const newDoc = await this.update({
|
||||||
position: targetPosition,
|
id,
|
||||||
|
position: this.getPrecisionPosition(targetPosition),
|
||||||
});
|
});
|
||||||
await this.set_envs();
|
|
||||||
|
await this.checkPosition(targetPosition, envs[toIndex].position!);
|
||||||
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async envs(
|
private async checkPosition(position: number, edge: number = 0) {
|
||||||
searchText: string = '',
|
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||||
sort: any = { position: -1 },
|
if (
|
||||||
query: any = {},
|
precisionPosition < minPosition ||
|
||||||
): Promise<Env[]> {
|
precisionPosition > maxPosition ||
|
||||||
|
Math.abs(precisionPosition - edge) < minPosition
|
||||||
|
) {
|
||||||
|
const envs = await this.envs();
|
||||||
|
let position = initPosition;
|
||||||
|
for (const env of envs) {
|
||||||
|
position = position - stepPosition;
|
||||||
|
await this.updateDb({ id: env.id, position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPrecisionPosition(position: number): number {
|
||||||
|
return parseFloat(position.toPrecision(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
|
||||||
let condition = { ...query };
|
let condition = { ...query };
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const reg = new RegExp(searchText);
|
const encodeText = encodeURIComponent(searchText);
|
||||||
|
const reg = {
|
||||||
|
[Op.or]: [
|
||||||
|
{ [Op.like]: `%${searchText}%` },
|
||||||
|
{ [Op.like]: `%${encodeText}%` },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
condition = {
|
condition = {
|
||||||
$or: [
|
...condition,
|
||||||
{
|
[Op.or]: [
|
||||||
value: reg,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: reg,
|
name: reg,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: reg,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
remarks: reg,
|
remarks: reg,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const newDocs = await this.find(condition, sort);
|
try {
|
||||||
return newDocs;
|
const result = await this.find(condition, [
|
||||||
|
['status', 'ASC'],
|
||||||
|
['position', 'DESC'],
|
||||||
|
['createdAt', 'ASC'],
|
||||||
|
]);
|
||||||
|
return result as any;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async find(query: any, sort: any): Promise<Env[]> {
|
private async find(query: any, sort: any = []): Promise<Env[]> {
|
||||||
return new Promise((resolve) => {
|
const docs = await EnvModel.findAll({
|
||||||
this.cronDb
|
where: { ...query },
|
||||||
.find(query)
|
order: [...sort],
|
||||||
.sort({ ...sort })
|
|
||||||
.exec((err, docs) => {
|
|
||||||
resolve(docs);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(_id: string): Promise<Env> {
|
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
||||||
return new Promise((resolve) => {
|
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
||||||
this.cronDb.find({ _id }).exec((err, docs) => {
|
return doc && (doc.get({ plain: true }) as Env);
|
||||||
resolve(docs[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getBySort(sort: any): Promise<Env> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.cronDb
|
|
||||||
.find({})
|
|
||||||
.sort({ ...sort })
|
|
||||||
.limit(1)
|
|
||||||
.exec((err, docs) => {
|
|
||||||
resolve(docs[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: string[]) {
|
public async disabled(ids: string[]) {
|
||||||
return new Promise((resolve: any) => {
|
await EnvModel.update(
|
||||||
this.cronDb.update(
|
{ status: EnvStatus.disabled },
|
||||||
{ _id: { $in: ids } },
|
{ where: { id: ids } },
|
||||||
{ $set: { status: EnvStatus.disabled } },
|
);
|
||||||
{ multi: true },
|
await this.set_envs();
|
||||||
async (err) => {
|
|
||||||
await this.set_envs();
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: string[]) {
|
public async enabled(ids: string[]) {
|
||||||
return new Promise((resolve: any) => {
|
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
||||||
this.cronDb.update(
|
await this.set_envs();
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ $set: { status: EnvStatus.normal } },
|
|
||||||
{ multi: true },
|
|
||||||
async (err, num) => {
|
|
||||||
await this.set_envs();
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
||||||
return new Promise((resolve: any) => {
|
await EnvModel.update({ name }, { where: { id: ids } });
|
||||||
this.cronDb.update(
|
await this.set_envs();
|
||||||
{ _id: { $in: ids } },
|
|
||||||
{ $set: { name } },
|
|
||||||
{ multi: true },
|
|
||||||
async (err, num) => {
|
|
||||||
await this.set_envs();
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async set_envs() {
|
public async set_envs() {
|
||||||
const envs = await this.envs(
|
const envs = await this.envs('', {
|
||||||
'',
|
name: { [Op.not]: null },
|
||||||
{ position: -1 },
|
status: EnvStatus.normal,
|
||||||
{ name: { $exists: true } },
|
});
|
||||||
);
|
const groups = groupBy(envs, 'name');
|
||||||
const groups = _.groupBy(envs, 'name');
|
|
||||||
let env_string = '';
|
let env_string = '';
|
||||||
for (const key in groups) {
|
for (const key in groups) {
|
||||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||||
const group = groups[key];
|
const group = groups[key];
|
||||||
env_string += `export ${key}="${_(group)
|
|
||||||
.filter((x) => x.status !== EnvStatus.disabled)
|
// 忽略不符合bash要求的环境变量名称
|
||||||
.map('value')
|
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
|
||||||
.join('&')
|
let value = group
|
||||||
.replace(/ /g, '')}"\n`;
|
.map((x) => x.value)
|
||||||
|
.join('&')
|
||||||
|
.replace(/(\\)[^n]/g, '\\\\')
|
||||||
|
.replace(/(\\$)/, '\\\\')
|
||||||
|
.replace(/"/g, '\\"')
|
||||||
|
.trim();
|
||||||
|
env_string += `export ${key}="${value}"\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fs.writeFileSync(config.envFile, env_string);
|
fs.writeFileSync(config.envFile, env_string);
|
||||||
|
|||||||
@@ -0,0 +1,494 @@
|
|||||||
|
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';
|
||||||
|
import { parseBody, parseHeaders } from '../config/util';
|
||||||
|
|
||||||
|
@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],
|
||||||
|
['chat', this.chat],
|
||||||
|
['bark', this.bark],
|
||||||
|
['telegramBot', this.telegramBot],
|
||||||
|
['dingtalkBot', this.dingtalkBot],
|
||||||
|
['weWorkBot', this.weWorkBot],
|
||||||
|
['weWorkApp', this.weWorkApp],
|
||||||
|
['aibotk', this.aibotk],
|
||||||
|
['iGot', this.iGot],
|
||||||
|
['pushPlus', this.pushPlus],
|
||||||
|
['email', this.email],
|
||||||
|
['webhook', this.webhook],
|
||||||
|
['lark', this.lark],
|
||||||
|
]);
|
||||||
|
|
||||||
|
private title = '';
|
||||||
|
private content = '';
|
||||||
|
private params!: Omit<NotificationInfo, 'type'>;
|
||||||
|
private gotOption = {
|
||||||
|
timeout: 30000,
|
||||||
|
retry: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
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}`, {
|
||||||
|
...this.gotOption,
|
||||||
|
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}`, {
|
||||||
|
...this.gotOption,
|
||||||
|
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, {
|
||||||
|
...this.gotOption,
|
||||||
|
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, pushDeerUrl } = this.params;
|
||||||
|
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
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 chat() {
|
||||||
|
const { chatUrl, chatToken } = this.params;
|
||||||
|
const url = `${chatUrl}${chatToken}`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, {
|
||||||
|
...this.gotOption,
|
||||||
|
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, {
|
||||||
|
...this.gotOption,
|
||||||
|
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, {
|
||||||
|
...this.gotOption,
|
||||||
|
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, {
|
||||||
|
...this.gotOption,
|
||||||
|
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 tokenRes: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
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=${tokenRes.access_token}`,
|
||||||
|
{
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
touser,
|
||||||
|
agentid,
|
||||||
|
safe: '0',
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.errcode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async aibotk() {
|
||||||
|
const { aibotkKey, aibotkType, aibotkName } = this.params;
|
||||||
|
let url = '';
|
||||||
|
let json = {};
|
||||||
|
switch (aibotkType) {
|
||||||
|
case 'room':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
|
||||||
|
json = {
|
||||||
|
apiKey: `${aibotkKey}`,
|
||||||
|
roomName: `${aibotkName}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'contact':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
|
||||||
|
json = {
|
||||||
|
apiKey: `${aibotkKey}`,
|
||||||
|
name: `${aibotkName}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
...json,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.code === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async iGot() {
|
||||||
|
const { iGotPushKey } = this.params;
|
||||||
|
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
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, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
token: `${pushPlusToken}`,
|
||||||
|
title: `${this.title}`,
|
||||||
|
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||||
|
topic: `${pushPlusUser || ''}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.code === 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async lark() {
|
||||||
|
const { larkKey } = this.params;
|
||||||
|
const res: any = await got
|
||||||
|
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
msg_type: 'text',
|
||||||
|
content: { text: `${this.title}\n\n${this.content}` },
|
||||||
|
},
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.StatusCode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async webhook() {
|
||||||
|
const {
|
||||||
|
webhookUrl,
|
||||||
|
webhookBody,
|
||||||
|
webhookHeaders,
|
||||||
|
webhookMethod,
|
||||||
|
webhookContentType,
|
||||||
|
} = this.params;
|
||||||
|
|
||||||
|
const { formatBody, formatUrl } = this.formatNotifyContent(
|
||||||
|
webhookUrl,
|
||||||
|
webhookBody,
|
||||||
|
);
|
||||||
|
if (!formatUrl && !formatBody) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const headers = parseHeaders(webhookHeaders);
|
||||||
|
const body = parseBody(formatBody, webhookContentType);
|
||||||
|
const bodyParam = this.formatBody(webhookContentType, body);
|
||||||
|
const options = {
|
||||||
|
method: webhookMethod,
|
||||||
|
headers,
|
||||||
|
...this.gotOption,
|
||||||
|
allowGetBody: true,
|
||||||
|
...bodyParam,
|
||||||
|
};
|
||||||
|
const res = await got(formatUrl, options);
|
||||||
|
return String(res.statusCode).startsWith('20');
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatBody(contentType: string, body: any): object {
|
||||||
|
if (!body) return {};
|
||||||
|
switch (contentType) {
|
||||||
|
case 'application/json':
|
||||||
|
return { json: body };
|
||||||
|
case 'multipart/form-data':
|
||||||
|
return { form: body };
|
||||||
|
case 'application/x-www-form-urlencoded':
|
||||||
|
return { body };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatNotifyContent(url: string, body: string) {
|
||||||
|
if (!url.includes('$title') && !body.includes('$title')) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatUrl: url
|
||||||
|
?.replaceAll('$title', encodeURIComponent(this.title))
|
||||||
|
?.replaceAll('$content', encodeURIComponent(this.content)),
|
||||||
|
formatBody: body
|
||||||
|
?.replaceAll('$title', this.title)
|
||||||
|
?.replaceAll('$content', this.content),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
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 generateSystemToken(): Promise<{
|
||||||
|
value: string;
|
||||||
|
expiration: number;
|
||||||
|
}> {
|
||||||
|
let systemApp = (await AppModel.findOne({
|
||||||
|
where: { name: 'system' },
|
||||||
|
})) as App;
|
||||||
|
if (!systemApp) {
|
||||||
|
systemApp = await this.create({
|
||||||
|
name: 'system',
|
||||||
|
scopes: ['crons', 'system'],
|
||||||
|
} as App);
|
||||||
|
}
|
||||||
|
const { data } = await this.authToken({
|
||||||
|
client_id: systemApp.client_id,
|
||||||
|
client_secret: systemApp.client_secret,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
value: data.token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
|
||||||
|
import {
|
||||||
|
ToadScheduler,
|
||||||
|
LongIntervalJob,
|
||||||
|
SimpleIntervalSchedule,
|
||||||
|
Task,
|
||||||
|
} 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 = {},
|
||||||
|
completionTime: 'start' | 'end' = 'end',
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
completionTime === 'start' && resolve(cp.pid);
|
||||||
|
|
||||||
|
cp.stdout.on('data', async (data) => {
|
||||||
|
await callbacks.onLog?.(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stderr.on('data', async (data) => {
|
||||||
|
this.logger.info(
|
||||||
|
'[执行任务失败] %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} 进程id: ${cp.pid},退出码 ${code}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('close', async (code) => {
|
||||||
|
const endTime = dayjs();
|
||||||
|
await callbacks.onEnd?.(
|
||||||
|
cp,
|
||||||
|
endTime,
|
||||||
|
endTime.diff(startTime, 'seconds'),
|
||||||
|
);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await this.logger.error(
|
||||||
|
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||||
|
command,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
await callbacks.onError?.(JSON.stringify(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (this.scheduleStacks.has(_id)) {
|
||||||
|
this.scheduleStacks.get(_id)?.cancel();
|
||||||
|
this.scheduleStacks.delete(_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Task(
|
||||||
|
name,
|
||||||
|
() => {
|
||||||
|
this.runTask(command, callbacks);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.logger.error(
|
||||||
|
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||||
|
command,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const job = new LongIntervalJob(
|
||||||
|
{ runImmediately: false, ...schedule },
|
||||||
|
task,
|
||||||
|
_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.intervalSchedule.addIntervalJob(job);
|
||||||
|
|
||||||
|
if (runImmediately) {
|
||||||
|
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,67 @@
|
|||||||
|
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';
|
||||||
|
import { TASK_COMMAND } from '../config/const';
|
||||||
|
import { getPid, killTask } from '../config/util';
|
||||||
|
|
||||||
|
@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_COMMAND} -l ${relativePath} now`;
|
||||||
|
const pid = await this.scheduleService.runTask(
|
||||||
|
command,
|
||||||
|
this.taskCallbacks(filePath),
|
||||||
|
'start',
|
||||||
|
);
|
||||||
|
|
||||||
|
return { code: 200, data: pid };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stopScript(filePath: string, pid: number) {
|
||||||
|
let str = '';
|
||||||
|
if (!pid) {
|
||||||
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
|
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await killTask(pid);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
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,114 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import fs, { existsSync } from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { Subscription } from '../data/subscription';
|
||||||
|
import { formatUrl } from '../config/subscription';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class SshKeyService {
|
||||||
|
private homedir = os.homedir();
|
||||||
|
private sshPath = config.sshdPath;
|
||||||
|
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');
|
||||||
|
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {
|
||||||
|
this.initSshConfigFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initSshConfigFile() {
|
||||||
|
let config = '';
|
||||||
|
if (existsSync(this.sshConfigFilePath)) {
|
||||||
|
config = fs.readFileSync(this.sshConfigFilePath, { encoding: 'utf-8' });
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(this.sshConfigFilePath, '');
|
||||||
|
}
|
||||||
|
if (!config.includes(this.sshConfigHeader)) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
this.sshConfigFilePath,
|
||||||
|
`${this.sshConfigHeader}\n\n${config}`,
|
||||||
|
{ encoding: 'utf-8' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generatePrivateKeyFile(alias: string, key: string): void {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
mode: '400',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('生成私钥文件失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removePrivateKeyFile(alias: string): void {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(this.sshPath, alias);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('删除私钥文件失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
|
||||||
|
if (host === 'github.com') {
|
||||||
|
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
|
||||||
|
}
|
||||||
|
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
|
||||||
|
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
|
||||||
|
this.sshPath,
|
||||||
|
alias,
|
||||||
|
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
||||||
|
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeSshConfig(alias: string) {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(this.sshPath, `${alias}.config`);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`删除ssh配置文件${alias}失败`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addSSHKey(
|
||||||
|
key: string,
|
||||||
|
alias: string,
|
||||||
|
host: string,
|
||||||
|
proxy?: string,
|
||||||
|
): void {
|
||||||
|
this.generatePrivateKeyFile(alias, key);
|
||||||
|
this.generateSingleSshConfig(alias, host, proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeSSHKey(alias: string, host: string, proxy?: string): void {
|
||||||
|
this.removePrivateKeyFile(alias);
|
||||||
|
this.removeSshConfig(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSshConfig(docs: Subscription[]) {
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
|
||||||
|
const { alias, proxy } = doc;
|
||||||
|
const { host } = formatUrl(doc);
|
||||||
|
this.removePrivateKeyFile(alias);
|
||||||
|
this.removeSshConfig(alias);
|
||||||
|
this.generatePrivateKeyFile(
|
||||||
|
alias,
|
||||||
|
(doc.pull_option as any).private_key,
|
||||||
|
);
|
||||||
|
this.generateSingleSshConfig(alias, host, proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
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 {
|
||||||
|
getFileContentByName,
|
||||||
|
concurrentRun,
|
||||||
|
fileExist,
|
||||||
|
createFile,
|
||||||
|
killTask,
|
||||||
|
} from '../config/util';
|
||||||
|
import { promises, existsSync } from 'fs';
|
||||||
|
import { FindOptions, Op } from 'sequelize';
|
||||||
|
import path from 'path';
|
||||||
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
|
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
||||||
|
import SockService from './sock';
|
||||||
|
import SshKeyService from './sshKey';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { LOG_END_SYMBOL } from '../config/const';
|
||||||
|
import { formatCommand, formatUrl } from '../config/subscription';
|
||||||
|
|
||||||
|
@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: [
|
||||||
|
['is_disabled', 'ASC'],
|
||||||
|
['createdAt', 'DESC'],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleTask(
|
||||||
|
doc: Subscription,
|
||||||
|
needCreate = true,
|
||||||
|
runImmediately = false,
|
||||||
|
) {
|
||||||
|
const { url } = formatUrl(doc);
|
||||||
|
|
||||||
|
doc.command = 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),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setSshConfig() {
|
||||||
|
const docs = await SubscriptionModel.findAll();
|
||||||
|
this.sshKeyService.setSshConfig(docs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async promiseExec(command: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
exec(
|
||||||
|
command,
|
||||||
|
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
|
||||||
|
(err, stdout, stderr) => {
|
||||||
|
resolve(stdout || stderr || JSON.stringify(err));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleLogPath(
|
||||||
|
logPath: string,
|
||||||
|
data: string = '',
|
||||||
|
): Promise<string> {
|
||||||
|
const absolutePath = path.resolve(config.logPath, logPath);
|
||||||
|
const logFileExist = await fileExist(absolutePath);
|
||||||
|
if (!logFileExist) {
|
||||||
|
await createFile(absolutePath, data);
|
||||||
|
}
|
||||||
|
return absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private taskCallbacks(doc: Subscription): TaskCallbacks {
|
||||||
|
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} 秒${LOG_END_SYMBOL}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
await this.setSshConfig();
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insert(payload: Subscription): Promise<Subscription> {
|
||||||
|
return await SubscriptionModel.create(payload, { returning: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(payload: Subscription): Promise<Subscription> {
|
||||||
|
const doc = await this.getDb({ id: payload.id })
|
||||||
|
const tab = new Subscription({ ...doc, ...payload });
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
await this.handleTask(newDoc, !newDoc.is_disabled);
|
||||||
|
await this.setSshConfig();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
await SubscriptionModel.destroy({ where: { id: ids } });
|
||||||
|
await this.setSshConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDb(query: FindOptions<Subscription>['where']): 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 {
|
||||||
|
await killTask(doc.pid);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.silly(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
||||||
|
|
||||||
|
fs.appendFileSync(
|
||||||
|
`${absolutePath}`,
|
||||||
|
`\n## 执行结束... ${dayjs().format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)}${LOG_END_SYMBOL}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SubscriptionModel.update(
|
||||||
|
{ status: SubscriptionStatus.idle, pid: undefined },
|
||||||
|
{ where: { id: ids } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runSingle(subscriptionId: number) {
|
||||||
|
const subscription = await this.getDb({ id: subscriptionId });
|
||||||
|
if (subscription.status !== SubscriptionStatus.queued) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = formatCommand(subscription);
|
||||||
|
|
||||||
|
await this.scheduleService.runTask(
|
||||||
|
command,
|
||||||
|
this.taskCallbacks(subscription),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disabled(ids: number[]) {
|
||||||
|
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
||||||
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
await this.setSshConfig();
|
||||||
|
for (const doc of docs) {
|
||||||
|
await this.handleTask(doc, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enabled(ids: number[]) {
|
||||||
|
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
||||||
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
await this.setSshConfig();
|
||||||
|
for (const doc of docs) {
|
||||||
|
await this.handleTask(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,173 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import config from '../config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
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';
|
||||||
|
import { parseContentVersion, parseVersion } from '../config/util';
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
this.scheduleService.createIntervalTask(cron, {
|
||||||
|
days: frequency,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { code: 200, data: { ...cron } };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async checkUpdate() {
|
||||||
|
try {
|
||||||
|
const currentVersionContent = await parseVersion(config.versionFile);
|
||||||
|
|
||||||
|
let lastVersionContent;
|
||||||
|
try {
|
||||||
|
const result = await got.get(
|
||||||
|
`${config.lastVersionFile}?t=${Date.now()}`,
|
||||||
|
{
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
lastVersionContent = await parseContentVersion(result.body);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
if (!lastVersionContent) {
|
||||||
|
lastVersionContent = currentVersionContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
hasNewVersion: this.checkHasNewVersion(
|
||||||
|
currentVersionContent.version,
|
||||||
|
lastVersionContent.version,
|
||||||
|
),
|
||||||
|
lastVersion: lastVersionContent.version,
|
||||||
|
lastLog: lastVersionContent.changeLog,
|
||||||
|
lastLogLink: lastVersionContent.changeLogLink,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} 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' });
|
||||||
|
|
||||||
|
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,365 @@
|
|||||||
|
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 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
const retriesTime = Math.pow(3, retries) * 1000;
|
||||||
|
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
||||||
|
const waitTime = Math.ceil(
|
||||||
|
(retriesTime - (timestamp - lastlogon)) / 1000,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
code: 410,
|
||||||
|
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||||
|
data: waitTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
username === cUsername &&
|
||||||
|
password === cPassword &&
|
||||||
|
twoFactorActivated &&
|
||||||
|
needTwoFactor
|
||||||
|
) {
|
||||||
|
this.updateAuthInfo(content, {
|
||||||
|
isTwoFactorChecking: true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
code: 420,
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ip, address } = await getNetIp(req);
|
||||||
|
if (username === cUsername && password === cPassword) {
|
||||||
|
const data = createRandomString(50, 100);
|
||||||
|
const expiration = twoFactorActivated ? 60 : 20;
|
||||||
|
let token = jwt.sign({ data }, config.secret as any, {
|
||||||
|
expiresIn: 60 * 60 * 24 * expiration,
|
||||||
|
algorithm: 'HS384',
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (retries > 2) {
|
||||||
|
const waitTime = Math.round(Math.pow(3, retries + 1));
|
||||||
|
return {
|
||||||
|
code: 410,
|
||||||
|
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||||
|
data: waitTime,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { code: 400, message: config.authError };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.initAuthInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(platform: string): Promise<any> {
|
||||||
|
const authInfo = 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
+40
@@ -0,0 +1,40 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import OpenService from './services/open';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import LoggerInstance from './loaders/logger';
|
||||||
|
import fs from 'fs';
|
||||||
|
import config from './config';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
const tokenFile = path.join(config.configPath, 'token.json');
|
||||||
|
|
||||||
|
async function getToken() {
|
||||||
|
try {
|
||||||
|
Container.set('logger', LoggerInstance);
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const appToken = await openService.generateSystemToken();
|
||||||
|
console.log(appToken.value);
|
||||||
|
await writeFile({
|
||||||
|
value: appToken.value,
|
||||||
|
expiration: appToken.expiration,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeFile(data: any) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
fs.writeFile(
|
||||||
|
tokenFile,
|
||||||
|
`${JSON.stringify(data)}${os.EOL}`,
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
() => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken();
|
||||||
@@ -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;
|
||||||
+47
-23
@@ -1,14 +1,30 @@
|
|||||||
FROM node:lts-alpine
|
FROM python:3.10-alpine as builder
|
||||||
LABEL maintainer="whyour"
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||||
ARG QL_URL=https://github.com/whyour/qinglong.git
|
RUN set -x \
|
||||||
ARG QL_BRANCH=master
|
&& apk update \
|
||||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
&& apk add nodejs npm git \
|
||||||
LANG=zh_CN.UTF-8 \
|
&& npm i -g pnpm \
|
||||||
|
&& cd /tmp/build \
|
||||||
|
&& pnpm install --prod
|
||||||
|
|
||||||
|
FROM python:3.10-alpine
|
||||||
|
|
||||||
|
ARG QL_MAINTAINER="whyour"
|
||||||
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
|
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||||
|
ARG QL_BRANCH=develop
|
||||||
|
|
||||||
|
ENV PNPM_HOME=/root/.local/share/pnpm \
|
||||||
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
|
||||||
|
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
|
||||||
|
LANG=C.UTF-8 \
|
||||||
SHELL=/bin/bash \
|
SHELL=/bin/bash \
|
||||||
PS1="\u@\h:\w \$ " \
|
PS1="\u@\h:\w \$ " \
|
||||||
QL_DIR=/ql
|
QL_DIR=/ql \
|
||||||
WORKDIR ${QL_DIR}
|
QL_BRANCH=${QL_BRANCH}
|
||||||
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 \
|
||||||
@@ -21,29 +37,37 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
|||||||
perl \
|
perl \
|
||||||
openssl \
|
openssl \
|
||||||
nginx \
|
nginx \
|
||||||
python3 \
|
nodejs \
|
||||||
jq \
|
jq \
|
||||||
openssh \
|
openssh \
|
||||||
|
npm \
|
||||||
&& 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 \
|
|
||||||
&& mkdir /run/nginx \
|
|
||||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
|
||||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||||
&& git config --global user.name "qinglong" \
|
&& git config --global user.name "qinglong" \
|
||||||
&& git config --global pull.rebase true \
|
&& git config --global http.postBuffer 524288000 \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& pnpm add -g pm2 tsx \
|
||||||
|
&& rm -rf /root/.pnpm-store \
|
||||||
|
&& rm -rf /root/.local/share/pnpm/store \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& rm -rf /root/.npm
|
||||||
|
|
||||||
|
ARG SOURCE_COMMIT
|
||||||
|
RUN 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 pnpm \
|
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||||
&& pnpm install -g pm2 \
|
&& mkdir -p ${QL_DIR}/static \
|
||||||
&& pnpm install -g ts-node typescript tslib \
|
&& cp -rf /static/* ${QL_DIR}/static \
|
||||||
&& rm -rf /root/.npm \
|
|
||||||
&& pnpm install --prod \
|
|
||||||
&& rm -rf /root/.pnpm-store \
|
|
||||||
&& git clone -b ${QL_BRANCH} https://github.com/whyour/qinglong-static.git /static \
|
|
||||||
&& cp -rf /static/* ${QL_DIR} \
|
|
||||||
&& rm -rf /static
|
&& rm -rf /static
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
|
||||||
|
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
|
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
|
||||||
+25
-22
@@ -1,16 +1,20 @@
|
|||||||
#!/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
|
||||||
|
|
||||||
|
export isFirstStartServer=true
|
||||||
|
|
||||||
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
|
||||||
pm2 l >/dev/null 2>&1
|
init_nginx
|
||||||
echo
|
|
||||||
|
pm2 l &>/dev/null
|
||||||
|
|
||||||
echo -e "======================2. 安装依赖========================\n"
|
echo -e "======================2. 安装依赖========================\n"
|
||||||
|
patch_version
|
||||||
update_depend
|
update_depend
|
||||||
echo
|
echo
|
||||||
|
|
||||||
@@ -18,31 +22,30 @@ 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 --source-map-support --time
|
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 --source-map-support --time
|
|
||||||
fi
|
|
||||||
echo -e "控制面板启动成功...\n"
|
|
||||||
|
|
||||||
echo -e "======================5. 启动定时任务========================\n"
|
echo -e "======================5. 启动主服务========================\n"
|
||||||
if [[ $(pm2 info schedule 2>/dev/null) ]]; then
|
pm2 delete panel &>/dev/null
|
||||||
pm2 reload schedule --source-map-support --time
|
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
|
||||||
else
|
echo -e "主服务启动成功...\n"
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
|
|
||||||
fi
|
echo -e "======================6. 启动定时服务========================\n"
|
||||||
|
pm2 delete schedule &>/dev/null
|
||||||
|
pm2 start $dir_static/build/schedule/index.js -n schedule --source-map-support --time
|
||||||
echo -e "定时任务启动成功...\n"
|
echo -e "定时任务启动成功...\n"
|
||||||
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
echo -e "======================6. 启动bot========================\n"
|
echo -e "======================7. 启动bot========================\n"
|
||||||
nohup ql bot >>$dir_log/start.log 2>&1 &
|
nohup ql -l bot >$dir_log/bot.log 2>&1 &
|
||||||
echo -e "bot后台启动中...\n"
|
echo -e "bot后台启动中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EnableExtraShell == true ]]; then
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
echo -e "======================7. 执行自定义脚本========================\n"
|
echo -e "======================8. 执行自定义脚本========================\n"
|
||||||
nohup ql extra >>$dir_log/start.log 2>&1 &
|
nohup ql -l extra >$dir_log/extra.log 2>&1 &
|
||||||
echo -e "自定义脚本后台执行中...\n"
|
echo -e "自定义脚本后台执行中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+44
-10
@@ -1,27 +1,61 @@
|
|||||||
upstream api {
|
upstream baseApi {
|
||||||
server 0.0.0.0:5600;
|
server 0.0.0.0:5600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstream publicApi {
|
||||||
|
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;
|
IPV6_CONFIG
|
||||||
|
root /ql/static/dist;
|
||||||
ssl_session_timeout 5m;
|
ssl_session_timeout 5m;
|
||||||
|
|
||||||
location /api {
|
location QL_BASE_URL/api/public/ {
|
||||||
proxy_pass http://api;
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass http://publicApi/api/public/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location QL_BASE_URL/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://baseApi/api/;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
location QL_BASE_URL/open/ {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass http://baseApi/open/;
|
||||||
}
|
}
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_static on;
|
gzip_static on;
|
||||||
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_comp_level 6;
|
gzip_comp_level 6;
|
||||||
gzip_buffers 16 8k;
|
gzip_buffers 16 8k;
|
||||||
gzip_http_version 1.0;
|
gzip_http_version 1.0;
|
||||||
|
|
||||||
location / {
|
location QL_BASE_URL/ {
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ QL_BASE_URL/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;
|
||||||
|
}
|
||||||
+123
-48
@@ -1,14 +1,19 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "umi dev",
|
"start": "concurrently -n w: npm:start:*",
|
||||||
"build": "umi build",
|
"start:front": "max dev",
|
||||||
"build-back": "tsc -p tsconfig.back.json",
|
"start:back": "nodemon",
|
||||||
"start-back": "nodemon",
|
"start:public": "ts-node --transpile-only ./back/public.ts",
|
||||||
"panel": "npm run build-back && node build/app.js",
|
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts",
|
||||||
"schedule": "npm run build-back && node build/schedule.js",
|
"build:front": "max build",
|
||||||
"prepare": "umi generate tmp",
|
"build:back": "tsc -p tsconfig.back.json",
|
||||||
|
"panel": "npm run build:back && node static/build/app.js",
|
||||||
|
"schedule": "npm run build:back && node static/build/schedule/index.js",
|
||||||
|
"public": "npm run build:back && node static/build/public.js",
|
||||||
|
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/cron.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
|
||||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||||
|
"postinstall": "max setup 2>/dev/null || true",
|
||||||
"test": "umi-test",
|
"test": "umi-test",
|
||||||
"test:coverage": "umi-test --coverage"
|
"test:coverage": "umi-test --coverage"
|
||||||
},
|
},
|
||||||
@@ -23,61 +28,131 @@
|
|||||||
"prettier --parser=typescript --write"
|
"prettier --parser=typescript --write"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"peerDependencyRules": {
|
||||||
|
"ignoreMissing": [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"antd",
|
||||||
|
"dva",
|
||||||
|
"postcss",
|
||||||
|
"webpack",
|
||||||
|
"eslint",
|
||||||
|
"stylelint",
|
||||||
|
"redux",
|
||||||
|
"@babel/core",
|
||||||
|
"monaco-editor",
|
||||||
|
"rc-field-form",
|
||||||
|
"@types/lodash.merge",
|
||||||
|
"rollup",
|
||||||
|
"styled-components"
|
||||||
|
],
|
||||||
|
"allowedVersions": {
|
||||||
|
"react": "18",
|
||||||
|
"react-dom": "18",
|
||||||
|
"dva-core": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.19.0",
|
"@grpc/grpc-js": "^1.8.13",
|
||||||
"celebrate": "^13.0.3",
|
"@otplib/preset-default": "^12.0.1",
|
||||||
|
"@sentry/node": "^7.12.1",
|
||||||
|
"@sentry/tracing": "^7.12.1",
|
||||||
|
"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",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"got": "^11.8.2",
|
||||||
|
"hpagent": "^0.1.2",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"multer": "^1.4.4",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-schedule": "^2.1.0",
|
||||||
"node-schedule": "^2.0.0",
|
"nodemailer": "^6.7.2",
|
||||||
"p-queue": "6.6.2",
|
"protobufjs": "^7.2.3",
|
||||||
|
"pstree.remy": "^1.1.8",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"typedi": "^0.8.0",
|
"sequelize": "^6.25.5",
|
||||||
"winston": "^3.3.3"
|
"serve-handler": "^6.1.3",
|
||||||
|
"sockjs": "^0.3.24",
|
||||||
|
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.2",
|
||||||
|
"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/icons": "^4.6.2",
|
"@ant-design/icons": "^4.7.0",
|
||||||
"@ant-design/pro-layout": "^6.5.0",
|
"@ant-design/pro-layout": "6.38.22",
|
||||||
"@monaco-editor/react": "^4.2.1",
|
"@monaco-editor/react": "4.2.1",
|
||||||
"@types/cors": "^2.8.10",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@types/express": "^4.17.8",
|
"@sentry/react": "^7.12.1",
|
||||||
"@types/express-jwt": "^6.0.1",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/lodash": "^4.14.170",
|
"@types/express": "^4.17.13",
|
||||||
"@types/nedb": "^1.8.11",
|
"@types/express-jwt": "^6.0.4",
|
||||||
"@types/node": "^14.11.2",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/node-fetch": "^2.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/qrcode.react": "^1.0.1",
|
"@types/lodash": "^4.14.185",
|
||||||
"@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.62.2",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"compression-webpack-plugin": "6.1.1",
|
"@types/qrcode.react": "^1.0.2",
|
||||||
"darkreader": "^4.9.27",
|
"@types/react": "^18.0.20",
|
||||||
"lint-staged": "^10.0.7",
|
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||||
"nodemon": "^2.0.4",
|
"@types/react-dom": "^18.0.6",
|
||||||
"prettier": "^2.2.0",
|
"@types/serve-handler": "^6.1.1",
|
||||||
|
"@types/sockjs": "^0.3.33",
|
||||||
|
"@types/sockjs-client": "^1.5.1",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
|
"@umijs/max": "^4.0.55",
|
||||||
|
"@umijs/ssr-darkreader": "^4.9.45",
|
||||||
|
"ansi-to-react": "^6.1.6",
|
||||||
|
"antd": "^4.24.8",
|
||||||
|
"antd-img-crop": "^4.2.3",
|
||||||
|
"codemirror": "^5.65.2",
|
||||||
|
"compression-webpack-plugin": "9.2.0",
|
||||||
|
"concurrently": "^7.0.0",
|
||||||
|
"lint-staged": "^13.0.3",
|
||||||
|
"monaco-editor": "0.33.0",
|
||||||
|
"nodemon": "^2.0.15",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"qiniu": "^7.4.0",
|
||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"react": "17.x",
|
"query-string": "^7.1.1",
|
||||||
|
"rc-tween-one": "^3.0.6",
|
||||||
|
"react": "18.2.0",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react-codemirror2": "^7.2.1",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-split-pane": "^0.1.92",
|
|
||||||
"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": "18.2.0",
|
||||||
"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",
|
"ts-proto": "^1.146.0",
|
||||||
|
"tslib": "^2.4.0",
|
||||||
|
"tsx": "^3.12.3",
|
||||||
|
"typescript": "4.8.4",
|
||||||
|
"umi-request": "^1.4.0",
|
||||||
"vh-check": "^2.0.5",
|
"vh-check": "^2.0.5",
|
||||||
"webpack": "^5.28.0",
|
"virtualizedtableforantd4": "1.3.0",
|
||||||
|
"webpack": "^5.70.0",
|
||||||
"yorkie": "^2.0.0"
|
"yorkie": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+11407
-5962
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.8 KiB |
@@ -1 +1 @@
|
|||||||
{ "username": "admin", "password": "adminadmin" }
|
{ "username": "admin", "password": "admin" }
|
||||||
|
|||||||
+73
-9
@@ -10,11 +10,19 @@ 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/SOCK5,例如 http://127.0.0.1:7890
|
||||||
GithubProxyUrl="https://ghproxy.com/"
|
ProxyUrl=""
|
||||||
|
|
||||||
|
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
|
||||||
|
CpuWarn=80
|
||||||
|
MemoryWarn=80
|
||||||
|
DiskWarn=90
|
||||||
|
|
||||||
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||||
CommandTimeoutTime="1h"
|
CommandTimeoutTime="1h"
|
||||||
@@ -26,14 +34,28 @@ MaxConcurrentNum="5"
|
|||||||
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
|
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
|
||||||
RandomDelay="300"
|
RandomDelay="300"
|
||||||
|
|
||||||
|
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
|
||||||
|
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
|
||||||
|
RandomDelayFileExtensions="js"
|
||||||
|
|
||||||
|
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
|
||||||
|
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
|
||||||
|
RandomDelayIgnoredMinutes="0 30"
|
||||||
|
|
||||||
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
|
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
|
||||||
EnableExtraShell="true"
|
EnableExtraShell="true"
|
||||||
|
|
||||||
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
|
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
|
||||||
AutoStartBot=""
|
AutoStartBot=""
|
||||||
|
|
||||||
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
|
||||||
PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
|
BotRepoUrl=""
|
||||||
|
|
||||||
|
## 安装python依赖时指定pip源
|
||||||
|
PipMirror="https://pypi.doubanio.com/simple/"
|
||||||
|
|
||||||
|
## 安装node依赖时指定npm源
|
||||||
|
NpmMirror="https://registry.npmmirror.com"
|
||||||
|
|
||||||
## 通知环境变量
|
## 通知环境变量
|
||||||
## 1. Server酱
|
## 1. Server酱
|
||||||
@@ -44,12 +66,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="https://qn.whyour.cn/logo.png"
|
||||||
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
|
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
|
||||||
export BARK_SOUND=""
|
export BARK_SOUND=""
|
||||||
## 下方填写推送消息分组,默认为"QingLong"
|
## 下方填写推送消息分组,默认为"QingLong"
|
||||||
export BARK_GROUP="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
|
||||||
@@ -70,7 +94,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=""
|
||||||
@@ -100,13 +124,53 @@ 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=""
|
||||||
|
|
||||||
|
## 10. gotify
|
||||||
|
## gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||||
|
## gotify_token 填写gotify的消息应用token
|
||||||
|
## gotify_priority 填写推送消息优先级,默认为0
|
||||||
|
export GOTIFY_URL=""
|
||||||
|
export GOTIFY_TOKEN=""
|
||||||
|
export GOTIFY_PRIORITY=0
|
||||||
|
|
||||||
|
## 11. PushDeer
|
||||||
|
## deer_key 填写PushDeer的key
|
||||||
|
export DEER_KEY=""
|
||||||
|
|
||||||
|
## 12. Chat
|
||||||
|
## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token=
|
||||||
|
## chat_token 填写后面的token
|
||||||
|
export CHAT_URL=""
|
||||||
|
export CHAT_TOKEN=""
|
||||||
|
|
||||||
|
## 13. aibotk
|
||||||
|
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
|
||||||
|
## aibotk_key (必填)填写智能微秘书个人中心的apikey
|
||||||
|
export AIBOTK_KEY=""
|
||||||
|
## aibotk_type (必填)填写发送的目标 room 或 contact, 填其他的不生效
|
||||||
|
export AIBOTK_TYPE=""
|
||||||
|
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
|
||||||
|
export AIBOTK_NAME=""
|
||||||
|
|
||||||
|
## 14. SMTP
|
||||||
|
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
|
||||||
|
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||||
|
export SMTP_SERVER=""
|
||||||
|
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
|
||||||
|
export SMTP_SSL="false"
|
||||||
|
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
||||||
|
export SMTP_EMAIL=""
|
||||||
|
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
|
export SMTP_PASSWORD=""
|
||||||
|
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
|
||||||
|
export SMTP_NAME=""
|
||||||
|
|
||||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 添加你需要重启自动执行的任意命令,比如 ql repo
|
## 添加你需要重启自动执行的任意命令,比如 ql repo
|
||||||
## 安装node依赖使用 pnpm install -g xxx xxx
|
## 安装node依赖使用 pnpm add -g xxx xxx
|
||||||
## 安装python依赖使用 pip3 install xxx
|
## 安装python依赖使用 pip3 install xxx
|
||||||
|
|||||||
+423
-116
@@ -13,23 +13,44 @@
|
|||||||
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 = '';
|
||||||
|
let PUSHDEER_URL = '';
|
||||||
|
|
||||||
|
// =======================================Synology Chat通知设置区域===========================================
|
||||||
|
//此处填你申请的CHAT_URL与CHAT_TOKEN
|
||||||
|
//(环境变量名 CHAT_URL CHAT_TOKEN)
|
||||||
|
let CHAT_URL = '';
|
||||||
|
let CHAT_TOKEN = '';
|
||||||
|
|
||||||
// =======================================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 = 'https://qn.whyour.cn/logo.png';
|
||||||
//BARK app推送铃声,铃声列表去APP查看复制填写
|
//BARK app推送铃声,铃声列表去APP查看复制填写
|
||||||
let BARK_SOUND = '';
|
let BARK_SOUND = '';
|
||||||
//BARK app推送消息的分组, 默认为"QingLong"
|
//BARK app推送消息的分组, 默认为"QingLong"
|
||||||
@@ -83,7 +104,50 @@ 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 = '';
|
||||||
|
|
||||||
|
// =======================================智能微秘书设置区域=======================================
|
||||||
|
//官方文档:http://wechat.aibotk.com/docs/about
|
||||||
|
//AIBOTK_KEY: 填写智能微秘书个人中心的apikey
|
||||||
|
//AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
|
||||||
|
//AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
|
||||||
|
let AIBOTK_KEY = '';
|
||||||
|
let AIBOTK_TYPE = '';
|
||||||
|
let AIBOTK_NAME = '';
|
||||||
|
|
||||||
|
// =======================================飞书机器人设置区域=======================================
|
||||||
|
//官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
|
||||||
|
//FSKEY 飞书机器人的 FSKEY
|
||||||
|
let FSKEY = '';
|
||||||
|
|
||||||
|
// =======================================SMTP 邮件设置区域=======================================
|
||||||
|
// SMTP_SERVER: 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||||
|
// SMTP_SSL: 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
|
||||||
|
// SMTP_EMAIL: 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
||||||
|
// SMTP_PASSWORD: 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
|
// SMTP_NAME: 填写 SMTP 收发件人姓名,可随意填写
|
||||||
|
let SMTP_SERVER = '';
|
||||||
|
let SMTP_SSL = 'false';
|
||||||
|
let SMTP_EMAIL = '';
|
||||||
|
let SMTP_PASSWORD = '';
|
||||||
|
let SMTP_NAME = '';
|
||||||
|
|
||||||
//==========================云端环境变量的判断与接收=========================
|
//==========================云端环境变量的判断与接收=========================
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -98,6 +162,19 @@ 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;
|
||||||
|
PUSHDEER_URL = process.env.DEER_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CHAT_URL) {
|
||||||
|
CHAT_URL = process.env.CHAT_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CHAT_TOKEN) {
|
||||||
|
CHAT_TOKEN = process.env.CHAT_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.QQ_SKEY) {
|
if (process.env.QQ_SKEY) {
|
||||||
QQ_SKEY = process.env.QQ_SKEY;
|
QQ_SKEY = process.env.QQ_SKEY;
|
||||||
}
|
}
|
||||||
@@ -116,6 +193,9 @@ if (process.env.BARK_PUSH) {
|
|||||||
} else {
|
} else {
|
||||||
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
|
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
|
||||||
}
|
}
|
||||||
|
if (process.env.BARK_ICON) {
|
||||||
|
BARK_ICON = process.env.BARK_ICON;
|
||||||
|
}
|
||||||
if (process.env.BARK_SOUND) {
|
if (process.env.BARK_SOUND) {
|
||||||
BARK_SOUND = process.env.BARK_SOUND;
|
BARK_SOUND = process.env.BARK_SOUND;
|
||||||
}
|
}
|
||||||
@@ -168,6 +248,36 @@ if (process.env.PUSH_PLUS_TOKEN) {
|
|||||||
if (process.env.PUSH_PLUS_USER) {
|
if (process.env.PUSH_PLUS_USER) {
|
||||||
PUSH_PLUS_USER = process.env.PUSH_PLUS_USER;
|
PUSH_PLUS_USER = process.env.PUSH_PLUS_USER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.AIBOTK_KEY) {
|
||||||
|
AIBOTK_KEY = process.env.AIBOTK_KEY;
|
||||||
|
}
|
||||||
|
if (process.env.AIBOTK_TYPE) {
|
||||||
|
AIBOTK_TYPE = process.env.AIBOTK_TYPE;
|
||||||
|
}
|
||||||
|
if (process.env.AIBOTK_NAME) {
|
||||||
|
AIBOTK_NAME = process.env.AIBOTK_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.FSKEY) {
|
||||||
|
FSKEY = process.env.FSKEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.SMTP_SERVER) {
|
||||||
|
SMTP_SERVER = process.env.SMTP_SERVER;
|
||||||
|
}
|
||||||
|
if (process.env.SMTP_SSL) {
|
||||||
|
SMTP_SSL = process.env.SMTP_SSL;
|
||||||
|
}
|
||||||
|
if (process.env.SMTP_EMAIL) {
|
||||||
|
SMTP_EMAIL = process.env.SMTP_EMAIL;
|
||||||
|
}
|
||||||
|
if (process.env.SMTP_PASSWORD) {
|
||||||
|
SMTP_PASSWORD = process.env.SMTP_PASSWORD;
|
||||||
|
}
|
||||||
|
if (process.env.SMTP_NAME) {
|
||||||
|
SMTP_NAME = process.env.SMTP_NAME;
|
||||||
|
}
|
||||||
//==========================云端环境变量的判断与接收=========================
|
//==========================云端环境变量的判断与接收=========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,6 +296,16 @@ async function sendNotify(
|
|||||||
) {
|
) {
|
||||||
//提供6种通知
|
//提供6种通知
|
||||||
desp += author; //增加作者信息,防止被贩卖等
|
desp += author; //增加作者信息,防止被贩卖等
|
||||||
|
|
||||||
|
// 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
|
||||||
|
let skipTitle = process.env.SKIP_PUSH_TITLE
|
||||||
|
if(skipTitle) {
|
||||||
|
if(skipTitle.split('\n').includes(text)) {
|
||||||
|
console.info(text + "在SKIP_PUSH_TITLE环境变量内,跳过推送!");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
serverNotify(text, desp), //微信server酱
|
serverNotify(text, desp), //微信server酱
|
||||||
pushPlusNotify(text, desp), //pushplus(推送加)
|
pushPlusNotify(text, desp), //pushplus(推送加)
|
||||||
@@ -199,53 +319,92 @@ 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
|
||||||
|
ChatNotify(text, desp), //synolog chat
|
||||||
|
PushDeerNotify(text, desp), //PushDeer
|
||||||
|
aibotkNotify(text, desp), //智能微秘书
|
||||||
|
fsBotNotify(text, desp), //飞书机器人
|
||||||
|
smtpNotify(text, desp), //SMTP 邮件
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function gobotNotify(text, desp, time = 2100) {
|
function gotifyNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (GOBOT_URL) {
|
if (GOTIFY_URL && GOTIFY_TOKEN) {
|
||||||
const options = {
|
const options = {
|
||||||
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
|
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
|
||||||
json: {message:`${text}\n${desp}`},
|
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(
|
||||||
|
desp,
|
||||||
|
)}&priority=${GOTIFY_PRIORITY}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
timeout,
|
|
||||||
};
|
};
|
||||||
setTimeout(() => {
|
$.post(options, (err, resp, data) => {
|
||||||
$.post(options, (err, resp, data) => {
|
try {
|
||||||
try {
|
if (err) {
|
||||||
if (err) {
|
console.log('gotify发送通知调用API失败!!\n');
|
||||||
console.log('发送go-cqhttp通知调用API失败!!\n');
|
console.log(err);
|
||||||
console.log(err);
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.id) {
|
||||||
|
console.log('gotify发送通知消息成功🎉\n');
|
||||||
} else {
|
} else {
|
||||||
data = JSON.parse(data);
|
console.log(`${data.message}\n`);
|
||||||
if (data.retcode === 0) {
|
|
||||||
console.log('go-cqhttp发送通知消息成功🎉\n');
|
|
||||||
} else if (data.retcode === 100) {
|
|
||||||
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
$.logErr(e, resp);
|
|
||||||
} finally {
|
|
||||||
resolve(data);
|
|
||||||
}
|
}
|
||||||
});
|
} catch (e) {
|
||||||
}, time);
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function serverNotify(text, desp, time = 2100) {
|
function gobotNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (GOBOT_URL) {
|
||||||
|
const options = {
|
||||||
|
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
|
||||||
|
json: { message: `${text}\n${desp}` },
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('发送go-cqhttp通知调用API失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.retcode === 0) {
|
||||||
|
console.log('go-cqhttp发送通知消息成功🎉\n');
|
||||||
|
} else if (data.retcode === 100) {
|
||||||
|
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
|
||||||
|
} else {
|
||||||
|
console.log(`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function serverNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (SCKEY) {
|
if (SCKEY) {
|
||||||
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
|
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
|
||||||
@@ -260,102 +419,63 @@ function serverNotify(text, desp, time = 2100) {
|
|||||||
},
|
},
|
||||||
timeout,
|
timeout,
|
||||||
};
|
};
|
||||||
setTimeout(() => {
|
$.post(options, (err, resp, data) => {
|
||||||
$.post(options, (err, resp, data) => {
|
try {
|
||||||
try {
|
if (err) {
|
||||||
if (err) {
|
console.log('发送通知调用API失败!!\n');
|
||||||
console.log('发送通知调用API失败!!\n');
|
console.log(err);
|
||||||
console.log(err);
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
//server酱和Server酱·Turbo版的返回json格式不太一样
|
||||||
|
if (data.errno === 0 || data.data.errno === 0) {
|
||||||
|
console.log('server酱发送通知消息成功🎉\n');
|
||||||
|
} else if (data.errno === 1024) {
|
||||||
|
// 一分钟内发送相同的内容会触发
|
||||||
|
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
|
||||||
} else {
|
} else {
|
||||||
data = JSON.parse(data);
|
console.log(`server酱发送通知消息异常\n${JSON.stringify(data)}`);
|
||||||
//server酱和Server酱·Turbo版的返回json格式不太一样
|
|
||||||
if (data.errno === 0 || data.data.errno === 0) {
|
|
||||||
console.log('server酱发送通知消息成功🎉\n');
|
|
||||||
} else if (data.errno === 1024) {
|
|
||||||
// 一分钟内发送相同的内容会触发
|
|
||||||
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`server酱发送通知消息异常\n${JSON.stringify(data)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
$.logErr(e, resp);
|
|
||||||
} finally {
|
|
||||||
resolve(data);
|
|
||||||
}
|
}
|
||||||
});
|
} catch (e) {
|
||||||
}, time);
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoolPush(text, desp) {
|
function PushDeerNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (QQ_SKEY) {
|
if (PUSHDEER_KEY) {
|
||||||
let options = {
|
// PushDeer 建议对消息内容进行 urlencode
|
||||||
url: `https://push.xuthus.cc/${QQ_MODE}/${QQ_SKEY}`,
|
desp = encodeURI(desp);
|
||||||
|
const options = {
|
||||||
|
url: PUSHDEER_URL || `https://api2.pushdeer.com/message/push`,
|
||||||
|
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
|
timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 已知敏感词
|
|
||||||
text = text.replace(/京豆/g, '豆豆');
|
|
||||||
desp = desp.replace(/京豆/g, '');
|
|
||||||
desp = desp.replace(/🐶/g, '');
|
|
||||||
desp = desp.replace(/红包/g, 'H包');
|
|
||||||
|
|
||||||
switch (QQ_MODE) {
|
|
||||||
case 'email':
|
|
||||||
options.json = {
|
|
||||||
t: text,
|
|
||||||
c: desp,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
options.body = `${text}\n\n${desp}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pushMode = function (t) {
|
|
||||||
switch (t) {
|
|
||||||
case 'send':
|
|
||||||
return '个人';
|
|
||||||
case 'group':
|
|
||||||
return 'QQ群';
|
|
||||||
case 'wx':
|
|
||||||
return '微信';
|
|
||||||
case 'ww':
|
|
||||||
return '企业微信';
|
|
||||||
case 'email':
|
|
||||||
return '邮件';
|
|
||||||
default:
|
|
||||||
return '未知方式';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$.post(options, (err, resp, data) => {
|
$.post(options, (err, resp, data) => {
|
||||||
try {
|
try {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(`发送${pushMode(QQ_MODE)}通知调用API失败!!\n`);
|
console.log('发送通知调用API失败!!\n');
|
||||||
console.log(err);
|
console.log(err);
|
||||||
} else {
|
} else {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
if (data.code === 200) {
|
// 通过返回的result的长度来判断是否成功
|
||||||
console.log(`酷推发送${pushMode(QQ_MODE)}通知消息成功🎉\n`);
|
if (
|
||||||
} else if (data.code === 400) {
|
data.content.result.length !== undefined &&
|
||||||
console.log(
|
data.content.result.length > 0
|
||||||
`QQ酷推(Cool Push)发送${pushMode(QQ_MODE)}推送失败:${
|
) {
|
||||||
data.msg
|
console.log('PushDeer发送通知消息成功🎉\n');
|
||||||
}\n`,
|
|
||||||
);
|
|
||||||
} else if (data.code === 503) {
|
|
||||||
console.log(`QQ酷推出错,${data.message}:${data.data}\n`);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`酷推推送异常: ${JSON.stringify(data)}`);
|
console.log(`PushDeer发送通知消息异常\n${JSON.stringify(data)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -370,13 +490,52 @@ function CoolPush(text, desp) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ChatNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (CHAT_URL && CHAT_TOKEN) {
|
||||||
|
// 对消息内容进行 urlencode
|
||||||
|
desp = encodeURI(desp);
|
||||||
|
const options = {
|
||||||
|
url: `${CHAT_URL}${CHAT_TOKEN}`,
|
||||||
|
body: `payload={"text":"${text}\n${desp}"}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('发送通知调用API失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.success) {
|
||||||
|
console.log('Chat发送通知消息成功🎉\n');
|
||||||
|
} else {
|
||||||
|
console.log(`Chat发送通知消息异常\n${JSON.stringify(data)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function BarkNotify(text, desp, params = {}) {
|
function BarkNotify(text, desp, params = {}) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (BARK_PUSH) {
|
if (BARK_PUSH) {
|
||||||
const options = {
|
const options = {
|
||||||
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
|
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
|
||||||
desp,
|
desp,
|
||||||
)}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(params)}`,
|
)}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
|
||||||
|
params,
|
||||||
|
)}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
@@ -412,9 +571,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,
|
||||||
};
|
};
|
||||||
@@ -466,7 +629,7 @@ function ddBotNotify(text, desp) {
|
|||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
content: ` ${text}\n\n${desp}`,
|
content: `${text}\n\n${desp}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
@@ -533,7 +696,7 @@ function qywxBotNotify(text, desp) {
|
|||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
content: ` ${text}\n\n${desp}`,
|
content: `${text}\n\n${desp}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
@@ -602,9 +765,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]) {
|
||||||
@@ -800,6 +963,150 @@ function pushPlusNotify(text, desp) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function aibotkNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (AIBOTK_KEY && AIBOTK_TYPE && AIBOTK_NAME) {
|
||||||
|
let json = {};
|
||||||
|
let url = '';
|
||||||
|
switch (AIBOTK_TYPE) {
|
||||||
|
case 'room':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
|
||||||
|
json = {
|
||||||
|
apiKey: `${AIBOTK_KEY}`,
|
||||||
|
roomName: `${AIBOTK_NAME}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${text}\n${desp}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'contact':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
|
||||||
|
json = {
|
||||||
|
apiKey: `${AIBOTK_KEY}`,
|
||||||
|
name: `${AIBOTK_NAME}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${text}\n${desp}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const options = {
|
||||||
|
url: url,
|
||||||
|
json,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('智能微秘书发送通知消息失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.code === 0) {
|
||||||
|
console.log('智能微秘书发送通知消息成功🎉。\n');
|
||||||
|
} else {
|
||||||
|
console.log(`${data.error}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fsBotNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (FSKEY) {
|
||||||
|
const options = {
|
||||||
|
url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,
|
||||||
|
json: { msg_type: 'text', content: { text: `${text}\n\n${desp}` } },
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('发送通知调用API失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.StatusCode === 0) {
|
||||||
|
console.log('飞书发送通知消息成功🎉\n');
|
||||||
|
} else {
|
||||||
|
console.log(`${data.msg}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function smtpNotify(text, desp) {
|
||||||
|
if (![SMTP_SERVER, SMTP_EMAIL, SMTP_PASSWORD].every(Boolean)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const transporter = nodemailer.createTransport(
|
||||||
|
`${SMTP_SSL === 'true' ? 'smtps:' : 'smtp:'}//${SMTP_SERVER}`,
|
||||||
|
{
|
||||||
|
auth: {
|
||||||
|
user: SMTP_EMAIL,
|
||||||
|
pass: SMTP_PASSWORD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: addr,
|
||||||
|
to: addr,
|
||||||
|
subject: text,
|
||||||
|
text: desp,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!!info.messageId) {
|
||||||
|
console.log('SMTP发送通知消息成功🎉\n');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log('SMTP发送通知消息失败!!\n');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('SMTP发送通知消息出现错误!!\n');
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function smtpNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
|
||||||
|
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendNotify,
|
sendNotify,
|
||||||
BARK_PUSH,
|
BARK_PUSH,
|
||||||
|
|||||||
+649
-241
@@ -1,277 +1,685 @@
|
|||||||
#!/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 smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.header import Header
|
||||||
|
from email.utils import formataddr
|
||||||
|
|
||||||
|
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
|
||||||
|
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
|
||||||
|
|
||||||
|
'CHAT_URL': '', # synology chat url
|
||||||
|
'CHAT_TOKEN': '', # synology chat token
|
||||||
|
|
||||||
|
'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
|
||||||
|
|
||||||
|
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
|
||||||
|
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
|
||||||
|
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
|
||||||
|
|
||||||
|
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||||
|
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
|
||||||
|
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
|
||||||
|
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
|
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
||||||
|
}
|
||||||
|
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").find("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'
|
||||||
|
if push_config.get("DEER_URL"):
|
||||||
|
url = push_config.get("DEER_URL")
|
||||||
|
|
||||||
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
|
if len(response.get("content").get("result")) > 0:
|
||||||
|
print("PushDeer 推送成功!")
|
||||||
|
else:
|
||||||
|
print("PushDeer 推送失败!错误信息:", response)
|
||||||
|
|
||||||
|
|
||||||
|
def chat(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过Chat 推送消息
|
||||||
|
"""
|
||||||
|
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
|
||||||
|
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("chat 服务启动")
|
||||||
|
data = 'payload=' + json.dumps({'text': title + '\n' + content})
|
||||||
|
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
|
||||||
|
response = requests.post(url, data=data)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("Chat 推送成功!")
|
||||||
|
else:
|
||||||
|
print("Chat 推送失败!错误信息:", 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)) # 时间戳
|
|
||||||
secret_enc = DD_BOT_SECRET.encode('utf-8')
|
|
||||||
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):
|
|
||||||
print("\n")
|
|
||||||
if not QYWX_APP:
|
|
||||||
print("企业微信应用的QYWX_APP未设置!!\n取消推送")
|
|
||||||
return
|
|
||||||
print("企业微信应用启动")
|
|
||||||
qywx_app_params = QYWX_APP.split(',')
|
|
||||||
url='https://qyapi.weixin.qq.com/cgi-bin/gettoken'
|
|
||||||
headers= {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
payload = {
|
payload = {
|
||||||
'corpid': qywx_app_params[0],
|
"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
|
||||||
'corpsecret': qywx_app_params[1],
|
response = requests.post(url=url, params=payload).json()
|
||||||
}
|
|
||||||
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
|
if response["code"] == 0:
|
||||||
if not qywx_app_params[4]:
|
print("qmsg 推送成功!")
|
||||||
options = {
|
|
||||||
'msgtype': 'text',
|
|
||||||
'text': {
|
|
||||||
content: f'{title}\n\n${content}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elif qywx_app_params[4] == '0':
|
|
||||||
options = {
|
|
||||||
'msgtype': 'textcard',
|
|
||||||
'textcard': {
|
|
||||||
title: f'{title}',
|
|
||||||
description: f'{content}',
|
|
||||||
btntxt: '更多'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elif qywx_app_params[4] == '1':
|
|
||||||
options = {
|
|
||||||
'msgtype': 'text',
|
|
||||||
'text': {
|
|
||||||
content: f'{title}\n\n${content}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
options = {
|
print(f'qmsg 推送失败!{response["reason"]}')
|
||||||
'msgtype': 'mpnews',
|
|
||||||
'mpnews': {
|
|
||||||
'articles': [
|
def wecom_app(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 企业微信 APP 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("QYWX_AM"):
|
||||||
|
print("QYWX_AM 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
||||||
|
if 4 < len(QYWX_AM_AY) > 5:
|
||||||
|
print("QYWX_AM 设置错误!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("企业微信 APP 服务启动")
|
||||||
|
|
||||||
|
corpid = QYWX_AM_AY[0]
|
||||||
|
corpsecret = QYWX_AM_AY[1]
|
||||||
|
touser = QYWX_AM_AY[2]
|
||||||
|
agentid = QYWX_AM_AY[3]
|
||||||
|
try:
|
||||||
|
media_id = QYWX_AM_AY[4]
|
||||||
|
except IndexError:
|
||||||
|
media_id = ""
|
||||||
|
wx = WeCom(corpid, corpsecret, agentid)
|
||||||
|
# 如果没有配置 media_id 默认就以 text 方式发送
|
||||||
|
if not media_id:
|
||||||
|
message = title + "\n\n" + content
|
||||||
|
response = wx.send_text(message, touser)
|
||||||
|
else:
|
||||||
|
response = wx.send_mpnews(title, content, media_id, touser)
|
||||||
|
|
||||||
|
if response == "ok":
|
||||||
|
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 aibotk(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 智能微秘书 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("AIBOTK_KEY") or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_NAME"):
|
||||||
|
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("智能微秘书 服务启动")
|
||||||
|
|
||||||
|
if push_config.get("AIBOTK_TYPE") == 'room':
|
||||||
|
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
|
||||||
|
data = {
|
||||||
|
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||||
|
"roomName": push_config.get("AIBOTK_NAME"),
|
||||||
|
"message": {"type": 1, "content": f'【青龙快讯】\n\n{title}\n{content}'}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
|
||||||
|
data = {
|
||||||
|
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||||
|
"name": push_config.get("AIBOTK_NAME"),
|
||||||
|
"message": {"type": 1, "content": f'【青龙快讯】\n\n{title}\n{content}'}
|
||||||
|
}
|
||||||
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
|
print(response)
|
||||||
|
if response["code"] == 0:
|
||||||
|
print("智能微秘书 推送成功!")
|
||||||
|
else:
|
||||||
|
print(f'智能微秘书 推送失败!{response["error"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def smtp(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 SMTP 邮件 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("SMTP_SERVER") or not push_config.get("SMTP_SSL") or not push_config.get("SMTP_EMAIL") or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_NAME"):
|
||||||
|
print("SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("SMTP 邮件 服务启动")
|
||||||
|
|
||||||
|
message = MIMEText(content, 'plain', 'utf-8')
|
||||||
|
message['From'] = formataddr((Header(push_config.get(
|
||||||
|
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
|
||||||
|
message['To'] = formataddr((Header(push_config.get(
|
||||||
|
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
|
||||||
|
message['Subject'] = Header(title, 'utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get(
|
||||||
|
"SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
|
||||||
|
smtp_server.login(push_config.get("SMTP_EMAIL"),
|
||||||
|
push_config.get("SMTP_PASSWORD"))
|
||||||
|
smtp_server.sendmail(push_config.get("SMTP_EMAIL"),
|
||||||
|
push_config.get("SMTP_EMAIL"), message.as_bytes())
|
||||||
|
smtp_server.close()
|
||||||
|
print("SMTP 邮件 推送成功!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f'SMTP 邮件 推送失败!{e}')
|
||||||
|
|
||||||
|
|
||||||
|
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("CHAT_URL") and push_config.get("CHAT_TOKEN"):
|
||||||
continue
|
notify_function.append(chat)
|
||||||
elif i == 'qywxapp_bot':
|
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
if QYWX_APP:
|
notify_function.append(pushplus_bot)
|
||||||
qywxapp_bot(title=title, content=content)
|
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||||
else:
|
notify_function.append(qmsg_bot)
|
||||||
print('未启用 企业微信应用推送')
|
if push_config.get("QYWX_AM"):
|
||||||
continue
|
notify_function.append(wecom_app)
|
||||||
else:
|
if push_config.get("QYWX_KEY"):
|
||||||
print('此类推送方式不存在')
|
notify_function.append(wecom_bot)
|
||||||
|
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
|
||||||
|
notify_function.append(telegram_bot)
|
||||||
|
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
|
||||||
|
notify_function.append(aibotk)
|
||||||
|
if push_config.get("SMTP_SERVER") and push_config.get("SMTP_SSL") and push_config.get("SMTP_EMAIL") and push_config.get("SMTP_PASSWORD") and push_config.get("SMTP_NAME"):
|
||||||
|
notify_function.append(smtp)
|
||||||
|
|
||||||
|
|
||||||
|
def send(title: str, content: str) -> None:
|
||||||
|
if not content:
|
||||||
|
print(f"{title} 推送内容为空!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
|
||||||
|
skipTitle = os.getenv("SKIP_PUSH_TITLE")
|
||||||
|
if skipTitle:
|
||||||
|
if (title in re.split("\n", skipTitle)):
|
||||||
|
print(f"{title} 在SKIP_PUSH_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()
|
||||||
|
|||||||
+2
-6
@@ -3,14 +3,10 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.0.0",
|
|
||||||
"download": "^8.0.0",
|
|
||||||
"http-server": "^0.12.3",
|
|
||||||
"got": "^11.5.1",
|
"got": "^11.5.1",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"nodemailer": "^6.8.0",
|
||||||
"request": "^2.88.2",
|
|
||||||
"tough-cookie": "^4.0.0",
|
"tough-cookie": "^4.0.0",
|
||||||
"tunnel": "0.0.6",
|
"tunnel": "0.0.6",
|
||||||
"ws": "^7.4.3"
|
"ws": "^7.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
requests
|
|
||||||
@@ -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.yaml';
|
||||||
|
const options = {
|
||||||
|
scope: `${process.env.QINIU_SCOPE}:${key}`,
|
||||||
|
};
|
||||||
|
const putPolicy = new qiniu.rs.PutPolicy(options);
|
||||||
|
const uploadToken = putPolicy.uploadToken(mac);
|
||||||
|
|
||||||
|
const localFile = 'version.yaml';
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
+210
-153
@@ -1,177 +1,234 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
create_token() {
|
||||||
|
local token_command="tsx ${dir_root}/back/token.ts"
|
||||||
|
local token_file="${dir_root}static/build/token.js"
|
||||||
|
if [[ -f $token_file ]]; then
|
||||||
|
token_command="node ${token_file}"
|
||||||
|
fi
|
||||||
|
token=$(eval "$token_command")
|
||||||
|
}
|
||||||
|
|
||||||
get_token() {
|
get_token() {
|
||||||
token=$(cat $file_auth_user | jq -r .token)
|
if [[ -f $file_auth_token ]]; then
|
||||||
|
token=$(cat $file_auth_token | jq -r .value)
|
||||||
|
local expiration=$(cat $file_auth_token | jq -r .expiration)
|
||||||
|
local currentTimeStamp=$(date +%s)
|
||||||
|
if [[ $currentTimeStamp -ge $expiration ]]; then
|
||||||
|
create_token
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
create_token
|
||||||
|
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}')
|
||||||
else
|
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
|
||||||
local schedule=$1
|
else
|
||||||
local command=$2
|
local schedule=$1
|
||||||
local name=$3
|
local command=$2
|
||||||
fi
|
local name=$3
|
||||||
|
local sub_id=$4
|
||||||
|
fi
|
||||||
|
|
||||||
local api=$(
|
if [[ ! $sub_id ]];then
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
|
sub_id="null"
|
||||||
-H "Accept: application/json" \
|
fi
|
||||||
-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" \
|
local api=$(
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
-H "Accept: application/json" \
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
-H "Authorization: Bearer $token" \
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
-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" \
|
||||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
--compressed
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
)
|
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||||
code=$(echo $api | jq -r .code)
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
message=$(echo $api | jq -r .message)
|
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
|
||||||
if [[ $code == 200 ]]; then
|
--compressed
|
||||||
echo -e "$name -> 添加成功"
|
)
|
||||||
else
|
code=$(echo "$api" | jq -r .code)
|
||||||
echo -e "$name -> 添加失败(${message})"
|
message=$(echo "$api" | jq -r .message)
|
||||||
fi
|
if [[ $code == 200 ]]; then
|
||||||
|
echo -e "$name -> 添加成功"
|
||||||
|
else
|
||||||
|
echo -e "$name -> 添加失败(${message})"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
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}')
|
||||||
local id=$(echo "$1" | awk -F ":" '{print $4}')
|
local id=$(echo "$1" | awk -F ":" '{print $4}')
|
||||||
else
|
else
|
||||||
local schedule=$1
|
local schedule=$1
|
||||||
local command=$2
|
local command=$2
|
||||||
local name=$3
|
local name=$3
|
||||||
local id=$4
|
local id=$4
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Authorization: Bearer $token" \
|
-H "Authorization: Bearer $token" \
|
||||||
-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://0.0.0.0:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://0.0.0.0: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)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code == 200 ]]; then
|
if [[ $code == 200 ]]; then
|
||||||
echo -e "$name -> 更新成功"
|
echo -e "$name -> 更新成功"
|
||||||
else
|
else
|
||||||
echo -e "$name -> 更新失败(${message})"
|
echo -e "$name -> 更新失败(${message})"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
local command=$1
|
local command=$1
|
||||||
local id=$2
|
local id=$2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Authorization: Bearer $token" \
|
-H "Authorization: Bearer $token" \
|
||||||
-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://0.0.0.0:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://0.0.0.0: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)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code == 200 ]]; then
|
if [[ $code == 200 ]]; then
|
||||||
echo -e "$command -> 更新成功"
|
echo -e "$command -> 更新成功"
|
||||||
else
|
else
|
||||||
echo -e "$command -> 更新失败(${message})"
|
echo -e "$command -> 更新失败(${message})"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
del_cron_api() {
|
del_cron_api() {
|
||||||
local ids=$1
|
local ids=$1
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
||||||
-X 'DELETE' \
|
-X 'DELETE' \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Authorization: Bearer $token" \
|
-H "Authorization: Bearer $token" \
|
||||||
-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://0.0.0.0:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://0.0.0.0: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
|
||||||
)
|
)
|
||||||
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_user_info() {
|
|
||||||
local currentTimeStamp=$(date +%s)
|
|
||||||
local api=$(
|
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
|
|
||||||
-H 'Accept: */*' \
|
|
||||||
-H "Authorization: Bearer $token" \
|
|
||||||
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
|
|
||||||
-H 'Referer: http://0.0.0.0:5700/crontab' \
|
|
||||||
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
|
|
||||||
--compressed
|
|
||||||
)
|
|
||||||
code=$(echo $api | jq -r .code)
|
|
||||||
if [[ $code != 200 ]]; then
|
|
||||||
echo -e "请先登录!"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update_cron() {
|
update_cron() {
|
||||||
local ids="$1"
|
local ids="$1"
|
||||||
local status="$2"
|
local status="$2"
|
||||||
local pid="${3:-''}"
|
local pid="${3:-''}"
|
||||||
local logPath="$4"
|
local logPath="$4"
|
||||||
local currentTimeStamp=$(date +%s)
|
local lastExecutingTime="${5:-0}"
|
||||||
local api=$(
|
local runningTime="${6:-0}"
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?t=$currentTimeStamp" \
|
local currentTimeStamp=$(date +%s)
|
||||||
-X 'PUT' \
|
local api=$(
|
||||||
-H "Accept: application/json" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
|
||||||
-H "Authorization: Bearer $token" \
|
-X 'PUT' \
|
||||||
-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 "Accept: application/json" \
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Authorization: Bearer $token" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
-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 "Referer: http://0.0.0.0:5700/crontab" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\"}" \
|
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||||
--compressed
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
)
|
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
|
||||||
code=$(echo $api | jq -r .code)
|
--compressed
|
||||||
message=$(echo $api | jq -r .message)
|
)
|
||||||
if [[ $code != 200 ]]; then
|
code=$(echo "$api" | jq -r .code)
|
||||||
echo -e "\n## 更新任务状态失败(${message})\n" >> $log_path
|
message=$(echo "$api" | jq -r .message)
|
||||||
fi
|
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
|
||||||
|
)
|
||||||
|
code=$(echo "$api" | jq -r .code)
|
||||||
|
message=$(echo "$api" | jq -r .message)
|
||||||
|
if [[ $code == 200 ]]; then
|
||||||
|
echo -e "通知发送成功"
|
||||||
|
else
|
||||||
|
echo -e "通知失败(${message})"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
find_cron_api() {
|
||||||
|
local params=$1
|
||||||
|
local currentTimeStamp=$(date +%s)
|
||||||
|
local api=$(
|
||||||
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
|
||||||
|
-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" \
|
||||||
|
--compressed
|
||||||
|
)
|
||||||
|
data=$(echo "$api" | jq -r .data)
|
||||||
|
if [[ $data == 'null' ]]; then
|
||||||
|
echo -e ""
|
||||||
|
else
|
||||||
|
name=$(echo "$api" | jq -r .data.name)
|
||||||
|
echo -e "$name"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
get_token
|
get_token
|
||||||
|
|||||||
Regular → Executable
+21
-16
@@ -1,27 +1,26 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 导入通用变量与函数
|
if [[ -z ${BotRepoUrl} ]]; then
|
||||||
dir_shell=/ql/shell
|
url="https://github.com/SuMaiKaDe/bot.git"
|
||||||
. $dir_shell/share.sh
|
repo_path="${dir_repo}/dockerbot"
|
||||||
url="${github_proxy_url}https://github.com/SuMaiKaDe/bot.git"
|
else
|
||||||
repo_path="${dir_repo}/dockerbot"
|
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
|
||||||
reset_romote_url ${repo_path} "${url}"
|
|
||||||
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"
|
||||||
|
|
||||||
@@ -29,14 +28,20 @@ echo -e "3、安装python3依赖...\n"
|
|||||||
if [[ $PipMirror ]]; then
|
if [[ $PipMirror ]]; then
|
||||||
pip3 config set global.index-url $PipMirror
|
pip3 config set global.index-url $PipMirror
|
||||||
fi
|
fi
|
||||||
cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
|
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
|
||||||
cd $dir_root
|
|
||||||
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"
|
||||||
|
|||||||
Regular → Executable
+20
-35
@@ -1,10 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
dir_shell=/ql/shell
|
|
||||||
. $dir_shell/share.sh
|
|
||||||
. $dir_shell/api.sh
|
|
||||||
get_token
|
|
||||||
|
|
||||||
reset_env() {
|
reset_env() {
|
||||||
echo -e "---> 1. 开始检测配置文件\n"
|
echo -e "---> 1. 开始检测配置文件\n"
|
||||||
fix_config
|
fix_config
|
||||||
@@ -18,38 +13,28 @@ reset_env() {
|
|||||||
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
|
||||||
echo -e "---> 脚本依赖安装完成\n"
|
echo -e "---> 脚本依赖安装完成\n"
|
||||||
|
}
|
||||||
|
|
||||||
echo -e "---> 4. 复制通知文件\n"
|
copy_dep() {
|
||||||
|
echo -e "---> 1. 复制通知文件\n"
|
||||||
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
|
||||||
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 -e "---> 通知文件复制完成\n"
|
echo -e "---> 通知文件复制完成\n"
|
||||||
}
|
|
||||||
|
|
||||||
reload_pm2() {
|
echo -e "---> 2. 复制nginx配置文件\n"
|
||||||
pm2 l >/dev/null 2>&1
|
init_nginx
|
||||||
|
echo -e "---> 配置文件复制完成\n"
|
||||||
if [[ $(pm2 info panel 2>/dev/null) ]]; then
|
|
||||||
pm2 reload panel --source-map-support --time >/dev/null 2>&1
|
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $(pm2 info schedule 2>/dev/null) ]]; then
|
|
||||||
pm2 reload schedule --source-map-support --time >/dev/null 2>&1
|
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pm2_log() {
|
pm2_log() {
|
||||||
echo -e "---> pm2日志"
|
echo -e "---> pm2日志"
|
||||||
local panelOut="/root/.pm2/logs/panel-out.log"
|
local panelOut="/root/.pm2/logs/panel-out.log"
|
||||||
local panelError="/root/.pm2/logs/panel-error.log"
|
local panelError="/root/.pm2/logs/panel-error.log"
|
||||||
tail -n 20 "$panelOut"
|
tail -n 100 "$panelOut"
|
||||||
tail -n 20 "$panelError"
|
tail -n 100 "$panelError"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_nginx() {
|
check_nginx() {
|
||||||
@@ -57,6 +42,7 @@ check_nginx() {
|
|||||||
echo -e "=====> 检测nginx服务\n$nginxPid"
|
echo -e "=====> 检测nginx服务\n$nginxPid"
|
||||||
if [[ $nginxPid ]]; then
|
if [[ $nginxPid ]]; then
|
||||||
echo -e "\n=====> nginx服务正常\n"
|
echo -e "\n=====> nginx服务正常\n"
|
||||||
|
nginx -s reload
|
||||||
else
|
else
|
||||||
echo -e "\n=====> nginx服务异常,重新启动nginx\n"
|
echo -e "\n=====> nginx服务异常,重新启动nginx\n"
|
||||||
nginx -c /etc/nginx/nginx.conf
|
nginx -c /etc/nginx/nginx.conf
|
||||||
@@ -78,9 +64,8 @@ check_pm2() {
|
|||||||
pm2_log
|
pm2_log
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/api/system?t=$currentTimeStamp" \
|
||||||
-H 'Accept: */*' \
|
-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 '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 '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' \
|
||||||
@@ -95,20 +80,20 @@ check_pm2() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
init_git() {
|
start_public() {
|
||||||
local dir_current=$(pwd)
|
echo -e "=====> 启动公开服务\n"
|
||||||
|
pm2 delete public --source-map-support --time &>/dev/null
|
||||||
cd $dir_root
|
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
|
||||||
git config --global user.email "qinglong@@users.noreply.github.com"
|
|
||||||
git config --global user.name "qinglong"
|
|
||||||
git config --global pull.rebase true
|
|
||||||
|
|
||||||
cd $dir_current
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
echo -e "=====> 开始检测"
|
echo -e "=====> 开始检测"
|
||||||
init_git
|
npm i -g pnpm
|
||||||
|
patch_version
|
||||||
|
pnpm add -g pm2 tsx
|
||||||
|
update_depend
|
||||||
|
start_public
|
||||||
|
copy_dep
|
||||||
check_ql
|
check_ql
|
||||||
check_nginx
|
check_nginx
|
||||||
check_pm2
|
check_pm2
|
||||||
|
|||||||
@@ -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}`);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user