mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cfcf8d194 | ||
|
|
d1e9f741d0 | ||
|
|
0cde9d07ee | ||
|
|
a266425348 | ||
|
|
6dbe02e321 | ||
|
|
39979879c9 | ||
|
|
ba3f604a5c | ||
|
|
9572582401 | ||
|
|
2bcef0ba75 | ||
|
|
31cd7c4007 | ||
|
|
1c4c1799b0 | ||
|
|
2d558df15e | ||
|
|
7b769da4e2 | ||
|
|
4e95d62933 | ||
|
|
83255280b9 | ||
|
|
2546cebdea | ||
|
|
f14a3fe0f1 | ||
|
|
c3eb845be4 | ||
|
|
1eef77dca5 | ||
|
|
d7d9f47fe5 | ||
|
|
fce6a996d9 | ||
|
|
48a484d7de | ||
|
|
4b81d3c612 | ||
|
|
21b6edd7eb | ||
|
|
9a43e022e9 | ||
|
|
62bef3f13d | ||
|
|
c87f5fa004 | ||
|
|
8d99f3ffbb | ||
|
|
100ca7b54b | ||
|
|
4a2624f12a | ||
|
|
21b01a85a8 | ||
|
|
e039ae436e | ||
|
|
b13a1a45f8 | ||
|
|
667fe2bbba | ||
|
|
f083871ca0 | ||
|
|
782e1ba235 | ||
|
|
b92c6edc4e | ||
|
|
bdf4657c73 | ||
|
|
8559438611 | ||
|
|
04045fd410 | ||
|
|
1c71a9d8a8 | ||
|
|
9a9ae9782c | ||
|
|
da2569850b | ||
|
|
b475035415 | ||
|
|
7baee27585 | ||
|
|
2cb2f1ce0a | ||
|
|
8c1407e2a9 | ||
|
|
7bcb113085 | ||
|
|
42dc27addf | ||
|
|
aac30398c4 | ||
|
|
ceae888372 | ||
|
|
61df7050db | ||
|
|
af5561cbbf | ||
|
|
bce30f922d | ||
|
|
d6ed394557 | ||
|
|
7b512a1681 | ||
|
|
7af0597cd5 | ||
|
|
2f5cf6d93c | ||
|
|
d672d2d104 | ||
|
|
c0d1fb1ef4 | ||
|
|
3c882ad0ce | ||
|
|
58acd2eb68 | ||
|
|
590473e7d2 | ||
|
|
21139157c8 | ||
|
|
8f8296ab64 | ||
|
|
40bf07d2a4 | ||
|
|
fd2b4fb86d | ||
|
|
783e867ef6 | ||
|
|
3873d9f978 | ||
|
|
7dece938c8 | ||
|
|
0842d66c79 | ||
|
|
d071e50d62 | ||
|
|
337f01aff1 | ||
|
|
5614e41eb0 | ||
|
|
4f0fdbe453 | ||
|
|
49c068db78 | ||
|
|
fbba44d482 | ||
|
|
10c07b0a09 | ||
|
|
20893698ba | ||
|
|
31de659ed3 | ||
|
|
6c08a16c66 | ||
|
|
21e7178f48 | ||
|
|
aefd267eb2 | ||
|
|
fb1a3980d4 | ||
|
|
cfc467f8a5 | ||
|
|
48120f012d | ||
|
|
c907a78437 | ||
|
|
915c0d09a9 | ||
|
|
80638862cc | ||
|
|
a7350948cd | ||
|
|
1e1be013b5 | ||
|
|
4062584eae | ||
|
|
abedddffa6 | ||
|
|
38858ccdb2 | ||
|
|
d56e6359d0 | ||
|
|
5886991464 | ||
|
|
838326616b | ||
|
|
fcf80fe946 | ||
|
|
2e63a7056c | ||
|
|
7c807e3ec8 | ||
|
|
b558aa0934 | ||
|
|
38757758cb | ||
|
|
84dd3d8fd1 | ||
|
|
19423c2d30 | ||
|
|
98bba7d7fa | ||
|
|
dcaa9ce000 | ||
|
|
3cae44a136 | ||
|
|
df8a3d2692 | ||
|
|
56e43f1808 | ||
|
|
2f6b95cccf | ||
|
|
ad9f6b2b64 | ||
|
|
165c3dc976 | ||
|
|
cbe6390b41 | ||
|
|
026eaf6606 | ||
|
|
ee184f8e62 | ||
|
|
5745231dc9 | ||
|
|
cbdc3a9861 | ||
|
|
ba5c42ad6d | ||
|
|
b056a731a1 | ||
|
|
b4cd94f2d9 | ||
|
|
3b8cb43f02 | ||
|
|
82cb11c3c4 | ||
|
|
eb7522a90b | ||
|
|
3898201b1e | ||
|
|
bb6ab0c17b | ||
|
|
652c40ab1f | ||
|
|
ca0d6215a9 | ||
|
|
61d155257c | ||
|
|
91399f1a1f | ||
|
|
2e738c5977 | ||
|
|
c0bd51cf03 | ||
|
|
ca5afe04f8 | ||
|
|
47db0b9f4e | ||
|
|
27f17a377a | ||
|
|
ecaf731ecf | ||
|
|
9fba2a85f6 | ||
|
|
93f75be182 | ||
|
|
d537901f59 | ||
|
|
260c1ed4a4 | ||
|
|
ac2bd52463 | ||
|
|
16c6f8fee1 | ||
|
|
8a0db21f17 | ||
|
|
9ab08a71c0 | ||
|
|
381c32a3b4 | ||
|
|
350c0f10c4 | ||
|
|
d5b982f813 | ||
|
|
6587375ff2 | ||
|
|
f5ecb81bad | ||
|
|
0141cd81c4 | ||
|
|
ff412c7497 | ||
|
|
623f239f91 | ||
|
|
c7ba304c6a | ||
|
|
c8393604d1 | ||
|
|
c5d47a67aa | ||
|
|
3a1423cd59 | ||
|
|
d57f4cf6cd | ||
|
|
af420942c4 | ||
|
|
eac4e5e66f | ||
|
|
b8822a98f7 | ||
|
|
95322b42c0 | ||
|
|
33f5a06d32 | ||
|
|
a7e5126c92 | ||
|
|
c62f3e5b8e | ||
|
|
5d3f413a05 | ||
|
|
644154ca38 | ||
|
|
cccce5ff74 | ||
|
|
aa1fe98188 | ||
|
|
b6a78550d6 | ||
|
|
70b7f2c493 | ||
|
|
ea44e5f209 | ||
|
|
41ef87d9f8 | ||
|
|
c0b4a70aa0 | ||
|
|
955875cb7a | ||
|
|
827e8571aa | ||
|
|
aed08b0f59 | ||
|
|
3ddaf41e27 | ||
|
|
38a9cce6bf | ||
|
|
ae255ef381 | ||
|
|
4316bef01e | ||
|
|
8491c5706d | ||
|
|
7b7fab1f32 | ||
|
|
5a6fccdc2f | ||
|
|
23668be46d | ||
|
|
f92f2c1666 | ||
|
|
895740a3cb | ||
|
|
4c76c3231f | ||
|
|
ca9d3f9559 | ||
|
|
3ca3c5e75c | ||
|
|
c2c0b1dfdc | ||
|
|
c633774487 | ||
|
|
c2d86fc517 | ||
|
|
d7c8d150ca | ||
|
|
6b8a4e65f2 | ||
|
|
482e43dd1f | ||
|
|
1437babc6c | ||
|
|
5ed882943a | ||
|
|
e678b3fb3a | ||
|
|
a5770bc4e4 | ||
|
|
59821bb939 | ||
|
|
b58040be44 | ||
|
|
be3b8ad52e | ||
|
|
0fdaf97461 | ||
|
|
f5f036372f | ||
|
|
3fceb983e6 | ||
|
|
ae6a39196d | ||
|
|
2810ff2753 | ||
|
|
205c0cf8d3 | ||
|
|
d9c801a091 | ||
|
|
0833aa3b30 | ||
|
|
9c90bbc146 | ||
|
|
231b387076 | ||
|
|
3de432652f | ||
|
|
cad714493e | ||
|
|
6a690a6108 | ||
|
|
1b85c52557 | ||
|
|
b37be92ea9 | ||
|
|
f95979f445 | ||
|
|
09e07218ae | ||
|
|
085f5a9451 | ||
|
|
eb4e935554 | ||
|
|
57986cf425 | ||
|
|
7bf14b9acc | ||
|
|
3ba0be34d9 | ||
|
|
0f828a3c75 | ||
|
|
16a768a142 | ||
|
|
f380d20279 | ||
|
|
be908ab36c | ||
|
|
b545454537 | ||
|
|
a5ff8dd1d6 | ||
|
|
59f23e518b | ||
|
|
858b3f0176 | ||
|
|
89b8494ad0 | ||
|
|
64ecd75bd5 | ||
|
|
138eb3393d | ||
|
|
1ad51d2925 | ||
|
|
317e175bca | ||
|
|
0e287aa7d1 | ||
|
|
28018b705b | ||
|
|
46ec72d6be | ||
|
|
82494a87af | ||
|
|
95c19b447a | ||
|
|
9c6eae936a | ||
|
|
77605a9360 | ||
|
|
82880c3f06 | ||
|
|
11143c771b | ||
|
|
0968eaa5a8 | ||
|
|
0687f9d472 | ||
|
|
25f0f5951a | ||
|
|
eec0ce65ad | ||
|
|
be4e100168 | ||
|
|
59e4f1063c | ||
|
|
359e21f02b | ||
|
|
6894bd9c0e | ||
|
|
7040f02823 | ||
|
|
41408de337 | ||
|
|
72e243dce8 | ||
|
|
77d4840e2a | ||
|
|
0d5961821c | ||
|
|
f34980b173 | ||
|
|
5b21a6e529 | ||
|
|
6dbb737d4b | ||
|
|
76ecfbaaa2 | ||
|
|
d9aacbcb92 | ||
|
|
e088325d95 | ||
|
|
b29dd2c580 | ||
|
|
89e82fd5b0 | ||
|
|
9f0b671d42 | ||
|
|
7de7c724dc | ||
|
|
3525b1d92a | ||
|
|
30cea84886 | ||
|
|
0c2cc7f575 | ||
|
|
87231fac25 | ||
|
|
2a5f0eee92 | ||
|
|
df2a659050 | ||
|
|
adc8f35e83 | ||
|
|
dfb0d73ef3 | ||
|
|
3a8101aad1 | ||
|
|
65d6432dad | ||
|
|
4929fa0440 | ||
|
|
7a02fe0d8e | ||
|
|
d06fccd0d1 | ||
|
|
8be71b129a | ||
|
|
1b02668014 | ||
|
|
3f536b5062 | ||
|
|
c3517ccc41 | ||
|
|
bdc3763a6f | ||
|
|
fcfdee4988 | ||
|
|
eb15e096e3 | ||
|
|
6e36e06dda | ||
|
|
cbd1ddc5e7 | ||
|
|
ebc2e0e4b6 | ||
|
|
95e104dc9c | ||
|
|
99f02e7c54 | ||
|
|
bc110ff949 | ||
|
|
69a5ba8996 | ||
|
|
25236958ff | ||
|
|
f2dbb64984 | ||
|
|
069e236674 | ||
|
|
e06175c3da | ||
|
|
dfab2cb7f1 | ||
|
|
f1e0823a10 | ||
|
|
5fbad9956f | ||
|
|
d6f5198893 | ||
|
|
603dc07581 | ||
|
|
ace820efd9 | ||
|
|
6ed4aa6bf4 | ||
|
|
9e4b41ce48 | ||
|
|
d8587b64a5 | ||
|
|
eba78ca84f | ||
|
|
5fd79173d6 | ||
|
|
b86ee5674d | ||
|
|
42fbed157f | ||
|
|
c51306ef27 | ||
|
|
e260dcb985 | ||
|
|
2ddf69a79a | ||
|
|
2f4fb657e6 | ||
|
|
2a9751e051 | ||
|
|
43e02905c8 | ||
|
|
635306e2d3 | ||
|
|
63f21b1698 | ||
|
|
59c9b17c40 | ||
|
|
dc5b4de5fa | ||
|
|
5d846beabd | ||
|
|
9728119101 | ||
|
|
38609feee9 | ||
|
|
0b99882193 | ||
|
|
e03a9d2c1d | ||
|
|
105c0f802f | ||
|
|
da70b0799a | ||
|
|
9d89ecc51e | ||
|
|
c77e302874 | ||
|
|
fd7153403d | ||
|
|
eff109bf81 | ||
|
|
1614f3c8ad | ||
|
|
7ced425e01 | ||
|
|
b183e9cd29 | ||
|
|
9320074a02 | ||
|
|
ecdfdff50d | ||
|
|
20ddb5a3cf | ||
|
|
9d8c592e69 | ||
|
|
66266fd545 | ||
|
|
cc238dedbc | ||
|
|
dac694776d | ||
|
|
1f42d0245c | ||
|
|
a6319698e2 | ||
|
|
9ab702e344 | ||
|
|
49045c39c7 | ||
|
|
52490bf320 | ||
|
|
faf982b156 | ||
|
|
cad1d86caf | ||
|
|
dd424237e2 | ||
|
|
0214c6f81b | ||
|
|
f7e1c2ae5e | ||
|
|
5e52398a43 | ||
|
|
7bc24a4b62 | ||
|
|
105073ec23 | ||
|
|
bbb1fc4e1b | ||
|
|
381427fd60 | ||
|
|
6c63fb83ef | ||
|
|
602ffdcd5a | ||
|
|
72240a7834 | ||
|
|
8336434d3f | ||
|
|
dd2d3127a0 | ||
|
|
c960717d92 | ||
|
|
23db56df57 | ||
|
|
ed95d66441 | ||
|
|
6cd0feaace | ||
|
|
22a23fe05e | ||
|
|
517fdcc8ec | ||
|
|
71d7d3818f | ||
|
|
7b7d62760e | ||
|
|
f554a5b1a0 | ||
|
|
f26d6f1449 | ||
|
|
0728f1f231 | ||
|
|
e687511f32 | ||
|
|
a8a2a54bea | ||
|
|
c1a3ac6e4d | ||
|
|
b1d55422ac | ||
|
|
f6630d84ab | ||
|
|
5c2bbc941f | ||
|
|
53123af832 | ||
|
|
41c3f21863 | ||
|
|
356f8db728 | ||
|
|
a14a8cc067 | ||
|
|
7ecd38ad1f | ||
|
|
4bc1deda42 | ||
|
|
f9c1dfc8cb | ||
|
|
7d417a11c9 | ||
|
|
a35c0cb64e | ||
|
|
1aaa0f0fa6 | ||
|
|
2fe8c38293 | ||
|
|
2a1adb2e0e | ||
|
|
5b76213354 | ||
|
|
ebcf8f96c8 | ||
|
|
0c3b3c206e | ||
|
|
ef16622a7e | ||
|
|
e7aff6d1d6 | ||
|
|
b1bee910b2 | ||
|
|
2049cf9aa9 | ||
|
|
6c3f848b2c | ||
|
|
2f8d31658b | ||
|
|
c77eb3d28d | ||
|
|
1a801c2d31 | ||
|
|
0a6c7e3719 | ||
|
|
9f5c8fa70a | ||
|
|
ecb035d0b4 | ||
|
|
b78b382ba1 | ||
|
|
49088f0d3a | ||
|
|
0abf657a9c | ||
|
|
eb2390135f | ||
|
|
0753ef9ce9 | ||
|
|
f3aca5575c | ||
|
|
54dd82dc77 | ||
|
|
86e5f208fb | ||
|
|
ce3543779e | ||
|
|
e11d0c25f3 | ||
|
|
e03c64dceb | ||
|
|
4d4d31431d | ||
|
|
c9fecae67a | ||
|
|
173b87a7b1 | ||
|
|
49ab1ad93a | ||
|
|
6c943ac97a | ||
|
|
325b423c4d | ||
|
|
164126ddc7 | ||
|
|
e728faf519 | ||
|
|
b00acc7ef5 | ||
|
|
e44a7aca1a | ||
|
|
aef57d94d6 | ||
|
|
9124f1acf3 | ||
|
|
01e0a89ba9 | ||
|
|
3052b48f51 | ||
|
|
d630f34505 | ||
|
|
2692351e0a | ||
|
|
0fade7a5a9 | ||
|
|
7ed1abde36 | ||
|
|
4f5540f756 | ||
|
|
4289a23895 | ||
|
|
f345f362e4 | ||
|
|
bc0044d119 | ||
|
|
849883f8d0 | ||
|
|
b3fc5bd366 | ||
|
|
5b9c38385a | ||
|
|
3232f1efa4 | ||
|
|
e217ca6d8f | ||
|
|
896a09d701 | ||
|
|
63cc3e59b4 | ||
|
|
f6d7c3c35e | ||
|
|
4336f6f175 | ||
|
|
8c7f1bc3d3 | ||
|
|
3e481900e2 | ||
|
|
031f1bc1ca | ||
|
|
51133e24ef | ||
|
|
736da7dfa0 | ||
|
|
95221e2b1d | ||
|
|
a53f788886 | ||
|
|
0360ba0b1e | ||
|
|
1923ceb5c2 | ||
|
|
424a1e6491 | ||
|
|
61963d5f5d | ||
|
|
f068cd6a46 | ||
|
|
5df860befe | ||
|
|
049a87780e | ||
|
|
3355963168 | ||
|
|
1824fd35fe | ||
|
|
69068ff899 | ||
|
|
8df18342c0 | ||
|
|
b6042c7e46 | ||
|
|
5989be7f25 | ||
|
|
c3c3dc3e06 | ||
|
|
1cbc534edc | ||
|
|
5b3c37d07c | ||
|
|
a5b0924036 | ||
|
|
26fdb626ea | ||
|
|
9e666f4348 | ||
|
|
64aadb7eb8 | ||
|
|
7f1c55c40a | ||
|
|
a8707e5422 | ||
|
|
a975166656 | ||
|
|
a4b2d84443 | ||
|
|
6549eeaf10 | ||
|
|
ce323d0703 | ||
|
|
e61f2e3ace | ||
|
|
42c71f4b8b | ||
|
|
73642ba11d | ||
|
|
8ebe049d82 | ||
|
|
94c2c407d8 | ||
|
|
e4f8c58eab | ||
|
|
9c1addae69 | ||
|
|
b601a3fdbf | ||
|
|
ee4528a8ef | ||
|
|
ea2c08be12 | ||
|
|
307207dddd | ||
|
|
ea56e44669 | ||
|
|
9eb614b17e | ||
|
|
2170628fa3 | ||
|
|
06595021cd | ||
|
|
e5ec982891 | ||
|
|
2f89494185 | ||
|
|
bcdca5084c | ||
|
|
68884b37a2 | ||
|
|
54f03a044e | ||
|
|
cf52721639 | ||
|
|
dc0225f100 | ||
|
|
6d32c86535 | ||
|
|
99085e5c95 | ||
|
|
fefe7782a9 | ||
|
|
e12f199ca7 | ||
|
|
630ced4451 | ||
|
|
6256acbfca | ||
|
|
e28d84dc6b | ||
|
|
09162d503a | ||
|
|
a4c79dfe3b | ||
|
|
92056d66b5 | ||
|
|
91e4905a3f | ||
|
|
4958d700de | ||
|
|
12216abcff | ||
|
|
e6869fefa4 | ||
|
|
703707c480 | ||
|
|
989bc51f0d | ||
|
|
f70c16be52 | ||
|
|
189184832e | ||
|
|
bb469d4847 | ||
|
|
a10d33adc7 | ||
|
|
1cf40624da | ||
|
|
65bc4c40e9 | ||
|
|
95f8e9953a | ||
|
|
177150cb75 | ||
|
|
a6c2a99c14 | ||
|
|
87be858220 | ||
|
|
78726ab832 | ||
|
|
ab35901ca6 | ||
|
|
05358ed1a4 | ||
|
|
421b9b8fb9 | ||
|
|
e1c04bb7fb | ||
|
|
c3c585bd6e | ||
|
|
12896659a8 | ||
|
|
e7355842d9 | ||
|
|
aa61fa7a45 | ||
|
|
57aa8440a9 | ||
|
|
f63464858d | ||
|
|
3d090d221a | ||
|
|
c931fbeaa9 | ||
|
|
364a582389 | ||
|
|
da74481ec4 | ||
|
|
c183201e6f | ||
|
|
fefdcb88fd | ||
|
|
a49d471204 | ||
|
|
89d9357a8a | ||
|
|
3574f69723 | ||
|
|
09ab572d79 | ||
|
|
427bf44e01 | ||
|
|
52b4633d46 | ||
|
|
7d5e215b18 | ||
|
|
7fb46f0079 | ||
|
|
9c5e6efff7 | ||
|
|
c3a53ddda9 | ||
|
|
d25ae98c68 | ||
|
|
0afd8fcbd6 | ||
|
|
c4a4d2550c | ||
|
|
dba341bdec | ||
|
|
e32a56df93 | ||
|
|
2ba99d9c47 | ||
|
|
6d44b0ef0c | ||
|
|
fc612859f1 | ||
|
|
d2c1cb5779 | ||
|
|
4abefccc5b | ||
|
|
394eccab5e | ||
|
|
5ddc9ac22e | ||
|
|
ed9db7c8d5 | ||
|
|
7aefcaf75e | ||
|
|
dfe51c6b73 | ||
|
|
9534bbe79e | ||
|
|
560195a54c | ||
|
|
7f66e0624d | ||
|
|
f59f1bb0c4 | ||
|
|
a8da02b74d | ||
|
|
d5964a857a | ||
|
|
8817342332 | ||
|
|
4367d8ecca | ||
|
|
e64f7f5426 | ||
|
|
b614d7ec7a | ||
|
|
ef26dd847e | ||
|
|
a264af0504 | ||
|
|
e3c1685203 | ||
|
|
006e0e2626 | ||
|
|
391be1987e | ||
|
|
cac0f21a99 | ||
|
|
57c1bbcae4 | ||
|
|
a50e3ad401 | ||
|
|
5acdd977bf | ||
|
|
1437c975ef | ||
|
|
48999a9358 | ||
|
|
1644fb97b4 | ||
|
|
3373911b7b | ||
|
|
633fe7707c | ||
|
|
092222b4cb | ||
|
|
49a3662788 | ||
|
|
d9db5442d3 | ||
|
|
ca2bda67b8 | ||
|
|
665ed19cff | ||
|
|
982643d6ef | ||
|
|
2bcd0112c4 | ||
|
|
e9f77829c8 | ||
|
|
25369e1aa8 | ||
|
|
af8d52744e | ||
|
|
5b3687f7b6 | ||
|
|
489846d2e6 |
@@ -1,6 +1,7 @@
|
||||
MONGODB_URI='mongodb://'
|
||||
YIYAN_MONGODB_URI=''
|
||||
|
||||
CRON_PORT=5500
|
||||
PORT=5600
|
||||
|
||||
LOG_LEVEL='debug'
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
name: Build And Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'develop'
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
schedule:
|
||||
# 参考 https://jasonet.co/posts/scheduled-actions/
|
||||
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
|
||||
# minute hour day month dayOfWeek
|
||||
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
|
||||
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-static:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@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: github.com/${{ github.repository_owner }}/qinglong-static
|
||||
GITHUB_BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
mkdir -p static
|
||||
cd ./static
|
||||
cp -rf ../dist ./ && cp -rf ../build ./
|
||||
git init -b ${GITHUB_BRANCH} && git add .
|
||||
git config --local user.name 'github-actions[bot]'
|
||||
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
||||
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||
|
||||
build:
|
||||
needs: build-static
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set time zone
|
||||
uses: szenius/set-timezone@v1.0
|
||||
with:
|
||||
timezoneLinux: "Asia/Shanghai"
|
||||
timezoneMacos: "Asia/Shanghai"
|
||||
timezoneWindows: "China Standard Time"
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
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@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
build-args: MAINTAINER=${{ github.repository_owner }}, QL_BRANCH=${{ github.ref_name }}
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
|
||||
context: docker/
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,24 +1,19 @@
|
||||
name: Publish Docker Image
|
||||
name: PublishDockerImage
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# https://github.com/docker/setup-qemu-action
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
@@ -32,23 +27,5 @@ jobs:
|
||||
- name: Docker buildx image and push on master branch
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/
|
||||
|
||||
- name: Replace tag without `v`
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: actions/github-script@v1
|
||||
id: version
|
||||
with:
|
||||
script: |
|
||||
return context.payload.ref.replace(/\/?refs\/tags\/v/, '')
|
||||
result-encoding: string
|
||||
|
||||
- name: Docker buildx image and push on release
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:${{steps.version.outputs.result}} docker/
|
||||
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/
|
||||
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
|
||||
|
||||
+4
-1
@@ -4,7 +4,7 @@
|
||||
/node_modules
|
||||
/npm-debug.log*
|
||||
/yarn-error.log
|
||||
/yarn.lock
|
||||
/pnpm-lock.yaml
|
||||
/package-lock.json
|
||||
|
||||
# production
|
||||
@@ -21,8 +21,11 @@
|
||||
/.env.local
|
||||
.env
|
||||
.history
|
||||
.version.ts
|
||||
|
||||
/config
|
||||
/log
|
||||
/db
|
||||
/manual_log
|
||||
/scripts
|
||||
/bak
|
||||
@@ -8,11 +8,15 @@ export default defineConfig({
|
||||
type: 'none',
|
||||
},
|
||||
fastRefresh: {},
|
||||
favicon: 'https://qinglong.whyour.cn/g5.ico',
|
||||
dynamicImport: {
|
||||
loading: '@/components/pageLoading',
|
||||
},
|
||||
favicon: '/images/g5.ico',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:5678/',
|
||||
target: 'http://127.0.0.1:5600/',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
chainWebpack: (config) => {
|
||||
@@ -28,14 +32,18 @@ export default defineConfig({
|
||||
externals: {
|
||||
react: 'window.React',
|
||||
'react-dom': 'window.ReactDOM',
|
||||
codemirror: 'window.CodeMirror',
|
||||
darkreader: 'window.DarkReader',
|
||||
codemirror: 'window.CodeMirror',
|
||||
'sockjs-client': 'window.SockJS',
|
||||
},
|
||||
scripts: [
|
||||
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
||||
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/lib/codemirror.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/darkreader@4.9.27/darkreader.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/mode/shell/shell.js',
|
||||
'https://cdn.jsdelivr.net/npm/darkreader@4/darkreader.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js',
|
||||
'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js',
|
||||
],
|
||||
});
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
# 安装说明
|
||||
|
||||
## 前置要求
|
||||
```bash
|
||||
1.已安装docker-ce
|
||||
2.选装docker-compose
|
||||
```
|
||||
## 安装方式1
|
||||
```bash
|
||||
1. 新建一个文件夹,用于存放相关数据
|
||||
2. 下载本仓库中的`docker-compose.yml`至本地,或是复制文件内容后在本地自行建立并粘贴内容
|
||||
3. 使用docker-compose启动
|
||||
4. 浏览器输入ip:5700即可进入面板
|
||||
|
||||
# 新建数据文件夹
|
||||
mkdir qinglong
|
||||
cd qinglong
|
||||
# 下载docker-compose.yml文件
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/develop/docker-compose.yml
|
||||
# 启动
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 安装方式2
|
||||
```bash
|
||||
# 复制下列命令在ssh执行(先安装docker)
|
||||
# 注:$PWD请修改为实际你想安装的路径
|
||||
docker run -dit \
|
||||
-v $PWD/ql/config:/ql/config \
|
||||
-v $PWD/ql/log:/ql/log \
|
||||
-v $PWD/ql/db:/ql/db \
|
||||
-v $PWD/ql/repo:/ql/repo \
|
||||
-v $PWD/ql/raw:/ql/raw \
|
||||
-v $PWD/ql/scripts:/ql/scripts \
|
||||
-v $PWD/ql/jbot:/ql/jbot \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
## 登录
|
||||
```bash
|
||||
|
||||
打开浏览器访问宿主机ip的5700端口即可
|
||||
例如http://192.168.100.123:5700即ip:5700
|
||||
|
||||
首次登录
|
||||
账号:admin 密码:admin
|
||||
会生成`auth.json`
|
||||
|
||||
在ssh输入
|
||||
1.docker exec -it qinglong bash
|
||||
2.cat /ql/config/auth.json
|
||||
|
||||
cat查看之后返回的结果类似如下字段
|
||||
|
||||
{"username":"admin","password":"Xb-ZYP526wmg4_h6q1WqIO"}
|
||||
# admin即为登录名;Xb-ZYP526wmg4_h6q1WqIO为登录密码
|
||||
```
|
||||
|
||||
输入此处记录的`username`及`password`,即可成功登录qinglong面板,登录后即可正常使用
|
||||
|
||||
## 拉取脚本
|
||||
```bash
|
||||
示例
|
||||
ql repo https://github.com/xxx.git #拉取仓库
|
||||
ql raw https://raw.githubusercontent.com/xxx #拉取单个脚本
|
||||
```
|
||||
## 备份
|
||||
|
||||
所有数据都将保存在`docker-compose.yml`所在的同级目录的`data`文件夹中,如需要备份,请直接备份`docker-compose.yml`及`data`文件夹即可
|
||||
|
||||
```bash
|
||||
root@debian:/opt/qinglong# ls -lah
|
||||
总用量 8.0K
|
||||
drwxr-xr-x 3 root root 4.0K 8月 30 01:29 .
|
||||
drwxr-xr-x 4 root root 4.0K 8月 30 00:51 ..
|
||||
drwxr-xr-x 8 root root 4.0K 8月 30 01:30 data
|
||||
-rw-r--r-- 1 root root 386 8月 30 01:29 docker-compose.yml
|
||||
```
|
||||
## 更新
|
||||
|
||||
在面板执行"更新面板"任务即可
|
||||
|
||||
或者
|
||||
```bash
|
||||
cd qinglong
|
||||
docker-compose down
|
||||
docker pull whyour/qinglong:latest
|
||||
docker-compose up -d
|
||||
```
|
||||
@@ -1,43 +1,49 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/whyour/qinglong">
|
||||
<img width="150" src="https://qinglong.whyour.cn/qinglong.png">
|
||||
<img width="150" src="/public/images/qinglong.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">青龙(WIP)</h1>
|
||||
<h1 align="center">青龙</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
python和javaScript的定时任务管理面板
|
||||
A timed task management panel that supports typescript, javaScript, python3, and shell.(支持python3、javaScript、shell、typescript 的定时任务管理面板)
|
||||
|
||||
[![donate][donate-image]][donate-url] [![build status][build-status-image]][build-status-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker version][docker-version-image]][docker-version-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![donate][donate-image]][donate-url]
|
||||
|
||||
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=for-the-badge
|
||||
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=flat
|
||||
[donate-url]: https://qinglong.whyour.cn/nice.png
|
||||
[build-status-image]: https://img.shields.io/docker/cloud/build/whyour/qinglong?style=for-the-badge
|
||||
[build-status-url]: https://img.shields.io/docker/cloud/build/whyour/qinglong
|
||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=for-the-badge
|
||||
[docker-pulls-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=for-the-badge
|
||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
||||
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
|
||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=for-the-badge
|
||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=for-the-badge
|
||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<img width="49%" src="/public/images/login.png">
|
||||
<img width="49%" src="/public/images/home.png">
|
||||
</p>
|
||||
|
||||
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](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)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
|
||||
|
||||
## 如何安装
|
||||
|
||||
请查看[INSTALL.md](INSTALL.md)
|
||||
|
||||
## 多谢
|
||||
|
||||
* [https://github.com/nevinee/jd_shell](https://github.com/nevinee/jd_shell)
|
||||
* [nevinee](https://gitee.com/evine)
|
||||
|
||||
* [https://github.com/alseambusher/crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
* [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
|
||||
* [Ant Design](https://ant.design)
|
||||
|
||||
@@ -46,19 +52,3 @@ python和javaScript的定时任务管理面板
|
||||
* [Umijs3.0](https://umijs.org)
|
||||
|
||||
* [darkreader](https://github.com/darkreader/darkreader)
|
||||
|
||||
## 免责声明
|
||||
|
||||
1. 此仓储脚本仅用于学习研究,不保证其合法性、准确性、有效性,请根据情况自行判断,本人对此不承担任何保证责任。
|
||||
|
||||
2. 由于此仓储脚本仅用于学习研究,您必须在下载后 24 小时内将所有内容从您的计算机或手机或任何存储设备中完全删除,若违反规定引起任何事件本人对此均不负责。
|
||||
|
||||
3. 请勿将此仓储脚本用于任何商业或非法目的,若违反规定请自行对此负责。
|
||||
|
||||
4. 此仓储脚本涉及应用与本人无关,本人对因此引起的任何隐私泄漏或其他后果不承担任何责任。
|
||||
|
||||
5. 本人对任何脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失和损害。
|
||||
|
||||
6. 如果任何单位或个人认为此仓储脚本可能涉嫌侵犯其权利,应及时通知并提供身份证明,所有权证明,我们将在收到认证文件确认后删除此仓储脚本。
|
||||
|
||||
7. 所有直接或间接使用、查看此仓储脚本的人均应该仔细阅读此声明。本人保留随时更改或补充此声明的权利。一旦您使用或复制了此仓储脚本,即视为您已接受此免责声明。
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import config from '../config';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { createPassword } from '../config/util';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
route.post(
|
||||
'/login',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let username = req.body.username;
|
||||
let password = req.body.password;
|
||||
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
|
||||
if (err) console.log(err);
|
||||
const authInfo = JSON.parse(data);
|
||||
if (username && password) {
|
||||
if (
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'adminadmin'
|
||||
) {
|
||||
const newPassword = createPassword(16, 22);
|
||||
fs.writeFileSync(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: authInfo.username,
|
||||
password: newPassword,
|
||||
}),
|
||||
);
|
||||
return res.send({
|
||||
code: 100,
|
||||
msg: '已初始化密码,请前往auth.json查看并重新登录',
|
||||
});
|
||||
}
|
||||
if (
|
||||
username == authInfo.username &&
|
||||
password == authInfo.password
|
||||
) {
|
||||
let token = jwt.sign(
|
||||
{ username, password },
|
||||
config.secret as any,
|
||||
{ expiresIn: 60 * 60 * 24 * 7, algorithm: 'HS384' },
|
||||
);
|
||||
fs.writeFileSync(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: authInfo.username,
|
||||
password: authInfo.password,
|
||||
token,
|
||||
}),
|
||||
);
|
||||
res.send({ code: 200, token });
|
||||
} else {
|
||||
res.send({ code: 400, msg: config.authError });
|
||||
}
|
||||
} else {
|
||||
res.send({ err: 400, msg: '请输入用户名密码!' });
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/logout',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
|
||||
if (err) console.log(err);
|
||||
const authInfo = JSON.parse(data);
|
||||
fs.writeFileSync(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: authInfo.username,
|
||||
password: authInfo.password,
|
||||
}),
|
||||
);
|
||||
res.send({ code: 200 });
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/user',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
fs.writeFile(config.authConfigFile, JSON.stringify(req.body), (err) => {
|
||||
if (err) console.log(err);
|
||||
res.send({ code: 200, msg: '更新成功' });
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/user',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
||||
if (err) console.log(err);
|
||||
const authInfo = JSON.parse(data);
|
||||
res.send({ code: 200, data: { username: authInfo.username } });
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
+42
-29
@@ -5,36 +5,49 @@ import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import { execSync } from 'child_process';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
|
||||
route.get(
|
||||
'/config/:key',
|
||||
'/configs/files',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let content = '未找到文件';
|
||||
switch (req.params.key) {
|
||||
case 'config':
|
||||
content = getFileContentByName(config.confFile);
|
||||
break;
|
||||
case 'sample':
|
||||
content = getFileContentByName(config.sampleFile);
|
||||
break;
|
||||
case 'crontab':
|
||||
content = getFileContentByName(config.crontabFile);
|
||||
break;
|
||||
case 'shareCode':
|
||||
let shareCodeFile = getLastModifyFilePath(config.shareCodeDir);
|
||||
content = getFileContentByName(shareCodeFile);
|
||||
break;
|
||||
case 'extra':
|
||||
content = getFileContentByName(config.extraFile);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
const fileList = fs.readdirSync(config.configPath, 'utf-8');
|
||||
res.send({
|
||||
code: 200,
|
||||
data: fileList
|
||||
.filter((x) => !config.blackFileList.includes(x))
|
||||
.map((x) => {
|
||||
return { title: x, value: x };
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/configs/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let content = '';
|
||||
if (config.blackFileList.includes(req.params.file)) {
|
||||
res.send({ code: 403, message: '文件无法访问' });
|
||||
}
|
||||
if (req.params.file.includes('sample')) {
|
||||
content = getFileContentByName(
|
||||
`${config.samplePath}${req.params.file}`,
|
||||
);
|
||||
} else {
|
||||
content = getFileContentByName(
|
||||
`${config.configPath}${req.params.file}`,
|
||||
);
|
||||
}
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
@@ -45,23 +58,23 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/save',
|
||||
'/configs/save',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
content: Joi.string().required(),
|
||||
content: Joi.string().allow('').optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const { name, content } = req.body;
|
||||
const path = (config.fileMap as any)[name];
|
||||
fs.writeFileSync(path, content);
|
||||
if (name === 'crontab.list') {
|
||||
execSync(`crontab ${path}`);
|
||||
if (config.blackFileList.includes(name)) {
|
||||
res.send({ code: 403, message: '文件无法访问' });
|
||||
}
|
||||
res.send({ code: 200, msg: '保存成功' });
|
||||
const path = `${config.configPath}${name}`;
|
||||
fs.writeFileSync(path, content);
|
||||
res.send({ code: 200, message: '保存成功' });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
|
||||
+118
-33
@@ -1,10 +1,9 @@
|
||||
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 CronService from '../services/cron';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import cron_parser from 'cron-parser';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
@@ -32,14 +31,36 @@ export default (app: Router) => {
|
||||
body: Joi.object({
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string(),
|
||||
name: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} else {
|
||||
return res.send({ code: 400, message: 'param schedule error' });
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/run',
|
||||
celebrate({
|
||||
body: 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.create(req.body);
|
||||
const data = await cronService.run(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -48,18 +69,16 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/:id/run',
|
||||
route.put(
|
||||
'/crons/stop',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
body: 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.run(req.params.id);
|
||||
const data = await cronService.stop(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -68,18 +87,16 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/:id/disable',
|
||||
route.put(
|
||||
'/crons/disable',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
body: 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.disabled(req.params.id);
|
||||
const data = await cronService.disabled(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -88,18 +105,16 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/:id/enable',
|
||||
route.put(
|
||||
'/crons/enable',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
body: 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.enabled(req.params.id);
|
||||
const data = await cronService.enabled(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -132,18 +147,25 @@ export default (app: Router) => {
|
||||
'/crons',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string(),
|
||||
command: Joi.string().optional(),
|
||||
schedule: Joi.string().optional(),
|
||||
name: Joi.string().optional(),
|
||||
_id: 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.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
if (
|
||||
!req.body.schedule ||
|
||||
cron_parser.parseExpression(req.body.schedule).hasNext()
|
||||
) {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} else {
|
||||
return res.send({ code: 400, message: 'param schedule error' });
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
@@ -152,17 +174,51 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/crons/:id',
|
||||
'/crons',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
body: 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.remove(req.params.id);
|
||||
const data = await cronService.remove(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/pin',
|
||||
celebrate({
|
||||
body: 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.pin(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/unpin',
|
||||
celebrate({
|
||||
body: 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.unPin(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -205,4 +261,33 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/status',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
ids: Joi.array().items(Joi.string().required()),
|
||||
status: Joi.string().required(),
|
||||
pid: Joi.string().optional(),
|
||||
log_path: Joi.string().optional(),
|
||||
last_running_time: Joi.number().optional(),
|
||||
last_execution_time: Joi.number().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.status({
|
||||
...req.body,
|
||||
status: parseInt(req.body.status),
|
||||
pid: parseInt(req.body.pid) || '',
|
||||
});
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
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('/', route);
|
||||
route.get(
|
||||
'/dependencies',
|
||||
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(
|
||||
'/dependencies',
|
||||
celebrate({
|
||||
body: Joi.array().items(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
type: Joi.number().required(),
|
||||
remark: Joi.number().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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/dependencies',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
_id: Joi.string().required(),
|
||||
type: Joi.number().required(),
|
||||
remark: Joi.number().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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/dependencies',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/dependencies/force',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const data = await dependenceService.removeDb(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/dependencies/:id',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const data = await dependenceService.get(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/dependencies/reinstall',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import CookieService from '../services/cookie';
|
||||
import EnvService from '../services/env';
|
||||
import { Logger } from 'winston';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
const route = Router();
|
||||
@@ -8,12 +8,12 @@ const route = Router();
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
route.get(
|
||||
'/cookies',
|
||||
'/envs',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CookieService);
|
||||
const data = await cookieService.cookies();
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.envs(req.query.searchValue as string);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -23,15 +23,21 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/cookies',
|
||||
'/envs',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()).min(1),
|
||||
body: Joi.array().items(
|
||||
Joi.object({
|
||||
value: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
remarks: Joi.string().optional().allow(''),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CookieService);
|
||||
const data = await cookieService.create(req.body);
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -41,18 +47,20 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/cookies',
|
||||
'/envs',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
value: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
remarks: Joi.string().optional().allow(''),
|
||||
_id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CookieService);
|
||||
const data = await cookieService.update(req.body);
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -62,17 +70,15 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/cookies/:id',
|
||||
'/envs',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CookieService);
|
||||
const data = await cookieService.remove(req.params.id);
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.remove(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -82,7 +88,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/cookies/:id/move',
|
||||
'/envs/:id/move',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
@@ -95,8 +101,65 @@ export default (app: Router) => {
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CookieService);
|
||||
const data = await cookieService.move(req.params.id, req.body);
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.move(req.params.id, req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/envs/disable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.disabled(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/envs/enable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.enabled(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/envs/name',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
ids: Joi.array().items(Joi.string().required()),
|
||||
name: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.updateNames(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -106,7 +169,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/cookies/:id/refresh',
|
||||
'/envs/:id',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
@@ -115,68 +178,8 @@ export default (app: Router) => {
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CookieService);
|
||||
const data = await cookieService.refreshCookie(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/cookies/:id/disable',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CookieService);
|
||||
const data = await cookieService.disabled(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/cookies/:id/enable',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CookieService);
|
||||
const data = await cookieService.enabled(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/cookies/:id',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CookieService);
|
||||
const data = await cookieService.get(req.params.id);
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.get(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
+10
-4
@@ -1,17 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
import auth from './auth';
|
||||
import cookie from './cookie';
|
||||
import user from './user';
|
||||
import env from './env';
|
||||
import config from './config';
|
||||
import log from './log';
|
||||
import cron from './cron';
|
||||
import script from './script';
|
||||
import open from './open';
|
||||
import dependence from './dependence';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
auth(app);
|
||||
cookie(app);
|
||||
user(app);
|
||||
env(app);
|
||||
config(app);
|
||||
log(app);
|
||||
cron(app);
|
||||
script(app);
|
||||
open(app);
|
||||
dependence(app);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/apps',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().optional().allow(''),
|
||||
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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/apps',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().optional().allow(''),
|
||||
scopes: Joi.array().items(Joi.string()),
|
||||
_id: 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.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/apps',
|
||||
celebrate({
|
||||
body: 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.remove(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/apps/:id/reset-secret',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: 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.resetSecret(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', 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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
fileExist,
|
||||
getFileContentByName,
|
||||
getLastModifyFilePath,
|
||||
} from '../config/util';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import path from 'path';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
|
||||
route.get(
|
||||
'/scripts/files',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const fileList = fs.readdirSync(config.scriptPath, 'utf-8');
|
||||
|
||||
let result = [];
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const fileOrDir = fileList[i];
|
||||
const fPath = path.join(config.scriptPath, fileOrDir);
|
||||
const dirStat = fs.statSync(fPath);
|
||||
if (['node_modules'].includes(fileOrDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dirStat.isDirectory()) {
|
||||
const childFileList = fs.readdirSync(fPath, 'utf-8');
|
||||
let children = [];
|
||||
for (let j = 0; j < childFileList.length; j++) {
|
||||
const childFile = childFileList[j];
|
||||
const sPath = path.join(config.scriptPath, fileOrDir, childFile);
|
||||
const _fileExist = await fileExist(sPath);
|
||||
if (_fileExist && fs.statSync(sPath).isFile()) {
|
||||
const statObj = fs.statSync(sPath);
|
||||
children.push({
|
||||
title: childFile,
|
||||
value: childFile,
|
||||
key: `${fileOrDir}-${childFile}`,
|
||||
mtime: statObj.mtimeMs,
|
||||
parent: fileOrDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
result.push({
|
||||
title: fileOrDir,
|
||||
value: fileOrDir,
|
||||
key: fileOrDir,
|
||||
mtime: dirStat.mtimeMs,
|
||||
disabled: true,
|
||||
children: children.sort((a, b) => b.mtime - a.mtime),
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
title: fileOrDir,
|
||||
value: fileOrDir,
|
||||
key: fileOrDir,
|
||||
mtime: dirStat.mtimeMs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.send({
|
||||
code: 200,
|
||||
data: result,
|
||||
});
|
||||
} 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 path = req.query.path ? `${req.query.path}/` : '';
|
||||
const content = getFileContentByName(
|
||||
`${config.scriptPath}${path}${req.params.file}`,
|
||||
);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/scripts',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
content: Joi.string().allow(''),
|
||||
originFilename: Joi.string().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, content, originFilename } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
content: string;
|
||||
originFilename: string;
|
||||
};
|
||||
if (!path) {
|
||||
path = config.scriptPath;
|
||||
}
|
||||
if (!path.endsWith('/')) {
|
||||
path += '/';
|
||||
}
|
||||
if (!path.startsWith('/')) {
|
||||
path = `${config.scriptPath}${path}`;
|
||||
}
|
||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||
return res.send({
|
||||
code: 430,
|
||||
data: '文件路径禁止访问',
|
||||
});
|
||||
}
|
||||
if (!originFilename) {
|
||||
originFilename = filename;
|
||||
}
|
||||
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
|
||||
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
||||
if (fs.existsSync(originFilePath)) {
|
||||
if (!fs.existsSync(config.bakPath)) {
|
||||
fs.mkdirSync(config.bakPath);
|
||||
}
|
||||
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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/scripts',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
content: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, content, path } = req.body as {
|
||||
filename: string;
|
||||
content: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = `${config.scriptPath}${path}/${filename}`;
|
||||
fs.writeFileSync(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/scripts',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = `${config.scriptPath}${path}/${filename}`;
|
||||
fs.unlinkSync(filePath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/scripts/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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,342 @@
|
||||
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 UserService from '../services/user';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', 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) {
|
||||
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 {
|
||||
const userService = Container.get(UserService);
|
||||
await userService.logout(req.platform);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user',
|
||||
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) {
|
||||
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 {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
res.send({
|
||||
code: 200,
|
||||
data: {
|
||||
username: authInfo.username,
|
||||
twoFactorActivated: authInfo.twoFactorActivated,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/user/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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/user/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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/user/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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/system',
|
||||
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 envDbContent = getFileContentByName(config.envDbFile);
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
Object.keys(authInfo).length === 2 &&
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'admin' &&
|
||||
envDbContent.length === 0
|
||||
) {
|
||||
isInitialized = false;
|
||||
}
|
||||
res.send({
|
||||
code: 200,
|
||||
data: {
|
||||
isInitialized,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 初始化api
|
||||
route.put(
|
||||
'/init/user',
|
||||
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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/init/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) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/system/log/remove',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const data = await userService.getLogRemoveFrequency();
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/system/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 userService = Container.get(UserService);
|
||||
const result = await userService.updateLogRemoveFrequency(
|
||||
req.body.frequency,
|
||||
);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/system/update-check',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const result = await userService.checkUpdate();
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/system/update',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const result = await userService.updateSystem();
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
+8
-2
@@ -9,9 +9,13 @@ import Logger from './loaders/logger';
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
|
||||
await require('./loaders').default({ expressApp: app });
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
|
||||
app
|
||||
await require('./loaders/db').default();
|
||||
|
||||
await require('./loaders/app').default({ expressApp: app });
|
||||
|
||||
const server = app
|
||||
.listen(config.port, () => {
|
||||
Logger.info(`
|
||||
################################################
|
||||
@@ -23,6 +27,8 @@ async function startServer() {
|
||||
Logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
await require('./loaders/server').default({ server });
|
||||
}
|
||||
|
||||
startServer();
|
||||
|
||||
+52
-16
@@ -1,59 +1,95 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { createRandomString } from './util';
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
const lastVersionFile =
|
||||
'https://ghproxy.com/https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
|
||||
|
||||
const envFound = dotenv.config();
|
||||
const rootPath = path.resolve(__dirname, '../../');
|
||||
const cookieFile = path.join(rootPath, 'config/cookie.sh');
|
||||
const rootPath = process.cwd();
|
||||
const envFile = path.join(rootPath, 'config/env.sh');
|
||||
const confFile = path.join(rootPath, 'config/config.sh');
|
||||
const sampleFile = path.join(rootPath, 'sample/config.sh.sample');
|
||||
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
|
||||
const crontabFile = path.join(rootPath, 'config/crontab.list');
|
||||
const confBakDir = path.join(rootPath, 'config/bak/');
|
||||
const authConfigFile = path.join(rootPath, 'config/auth.json');
|
||||
const shareCodeDir = path.join(rootPath, 'log/export_sharecodes/');
|
||||
const extraFile = path.join(rootPath, 'config/extra.sh');
|
||||
const configPath = path.join(rootPath, 'config/');
|
||||
const scriptPath = path.join(rootPath, 'scripts/');
|
||||
const bakPath = path.join(rootPath, 'bak/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const logPath = path.join(rootPath, 'log/');
|
||||
const authError = '错误的用户名密码,请重试';
|
||||
const loginFaild = '请先登录!';
|
||||
const configString = 'config sample crontab shareCode diy';
|
||||
const dbPath = path.join(rootPath, 'db/');
|
||||
const manualLogPath = path.join(rootPath, 'manual_log/');
|
||||
const cronDbFile = path.join(rootPath, 'db/crontab.db');
|
||||
const cookieDbFile = path.join(rootPath, 'db/cookie.db');
|
||||
const envDbFile = path.join(rootPath, 'db/env.db');
|
||||
const appDbFile = path.join(rootPath, 'db/app.db');
|
||||
const authDbFile = path.join(rootPath, 'db/auth.db');
|
||||
const dependenceDbFile = path.join(rootPath, 'db/dependence.db');
|
||||
const versionFile = path.join(rootPath, 'src/version.ts');
|
||||
|
||||
const configFound = dotenv.config({ path: confFile });
|
||||
|
||||
if (envFound.error) {
|
||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||
}
|
||||
|
||||
if (configFound.error) {
|
||||
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
|
||||
}
|
||||
|
||||
export default {
|
||||
port: parseInt(process.env.PORT as string, 10),
|
||||
secret: process.env.SECRET,
|
||||
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
||||
secret: process.env.SECRET || createRandomString(16, 32),
|
||||
logs: {
|
||||
level: process.env.LOG_LEVEL || 'silly',
|
||||
},
|
||||
api: {
|
||||
prefix: '/api',
|
||||
},
|
||||
rootPath,
|
||||
configString,
|
||||
loginFaild,
|
||||
authError,
|
||||
logPath,
|
||||
extraFile,
|
||||
shareCodeDir,
|
||||
authConfigFile,
|
||||
confBakDir,
|
||||
crontabFile,
|
||||
sampleFile,
|
||||
confFile,
|
||||
cookieFile,
|
||||
fileMap: {
|
||||
'config.sh': confFile,
|
||||
'crontab.list': crontabFile,
|
||||
'extra.sh': extraFile,
|
||||
},
|
||||
envFile,
|
||||
dbPath,
|
||||
cronDbFile,
|
||||
cookieDbFile,
|
||||
manualLogPath,
|
||||
envDbFile,
|
||||
appDbFile,
|
||||
authDbFile,
|
||||
dependenceDbFile,
|
||||
configPath,
|
||||
scriptPath,
|
||||
samplePath,
|
||||
blackFileList: [
|
||||
'auth.json',
|
||||
'config.sh.sample',
|
||||
'cookie.sh',
|
||||
'crontab.list',
|
||||
'env.sh',
|
||||
],
|
||||
writePathList: [configPath, scriptPath],
|
||||
bakPath,
|
||||
apiWhiteList: [
|
||||
'/api/login',
|
||||
'/open/auth/token',
|
||||
'/api/user/two-factor/login',
|
||||
'/api/system',
|
||||
'/api/init/user',
|
||||
'/api/init/notification',
|
||||
],
|
||||
versionFile,
|
||||
lastVersionFile,
|
||||
};
|
||||
|
||||
+122
-2
@@ -1,5 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import got from 'got';
|
||||
import iconv from 'iconv-lite';
|
||||
|
||||
export function getFileContentByName(fileName: string) {
|
||||
if (fs.existsSync(fileName)) {
|
||||
@@ -27,7 +29,7 @@ export function getLastModifyFilePath(dir: string) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
export function createPassword(min: number, max: number): string {
|
||||
export function createRandomString(min: number, max: number): string {
|
||||
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
const english = [
|
||||
'a',
|
||||
@@ -85,7 +87,7 @@ export function createPassword(min: number, max: number): string {
|
||||
'Y',
|
||||
'Z',
|
||||
];
|
||||
const special = ['-', '_', '#'];
|
||||
const special = ['-', '_'];
|
||||
const config = num.concat(english).concat(ENGLISH).concat(special);
|
||||
|
||||
const arr = [];
|
||||
@@ -111,3 +113,121 @@ export function createPassword(min: number, max: number): string {
|
||||
|
||||
return newArr.join('');
|
||||
}
|
||||
|
||||
export function getToken(req: any) {
|
||||
const { authorization = '' } = req.headers;
|
||||
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
||||
return (authorization as string)
|
||||
.replace('Bearer ', '')
|
||||
.replace('mobile-', '')
|
||||
.replace('desktop-', '');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function getNetIp(req: any) {
|
||||
const ipArray = [
|
||||
...new Set([
|
||||
...(req.headers['x-real-ip'] || '').split(','),
|
||||
...(req.headers['x-forwarded-for'] || '').split(','),
|
||||
req.ip,
|
||||
...req.ips,
|
||||
req.socket.remoteAddress,
|
||||
]),
|
||||
];
|
||||
let ip = ipArray[0];
|
||||
|
||||
if (ipArray.length > 1) {
|
||||
for (let i = 0; i < ipArray.length; i++) {
|
||||
const ipNumArray = ipArray[i].split('.');
|
||||
const tmp = ipNumArray[0] + '.' + ipNumArray[1];
|
||||
if (
|
||||
tmp === '192.168' ||
|
||||
(ipNumArray[0] === '172' &&
|
||||
ipNumArray[1] >= 16 &&
|
||||
ipNumArray[1] <= 32) ||
|
||||
tmp === '10.7' ||
|
||||
tmp === '127.0'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
ip = ipArray[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
|
||||
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
||||
ip = '';
|
||||
}
|
||||
try {
|
||||
const baiduApi = got
|
||||
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
||||
.text();
|
||||
const ipApi = got
|
||||
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
|
||||
timeout: 10000,
|
||||
retry: 0,
|
||||
})
|
||||
.buffer();
|
||||
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
|
||||
|
||||
const ipRegx = /.*IP :(.*)\n/;
|
||||
const addrRegx = /.*数据二 :(.*)\n/;
|
||||
if (data && ipRegx.test(data) && addrRegx.test(data)) {
|
||||
const ip = data.match(ipRegx)[1];
|
||||
const addr = data.match(addrRegx)[1];
|
||||
return { address: addr, ip };
|
||||
} else if (ipApiBody) {
|
||||
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
|
||||
return { address: `${addr}`, ip };
|
||||
} else {
|
||||
return { address: `获取失败`, ip };
|
||||
}
|
||||
} catch (error) {
|
||||
return { address: `获取失败`, ip };
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
||||
const ua = userAgent.toLowerCase();
|
||||
const testUa = (regexp: RegExp) => regexp.test(ua);
|
||||
const testVs = (regexp: RegExp) =>
|
||||
(ua.match(regexp) || [])
|
||||
.toString()
|
||||
.replace(/[^0-9|_.]/g, '')
|
||||
.replace(/_/g, '.');
|
||||
|
||||
// 系统
|
||||
let system = 'unknow';
|
||||
if (testUa(/windows|win32|win64|wow32|wow64/g)) {
|
||||
system = 'windows'; // windows系统
|
||||
} else if (testUa(/macintosh|macintel/g)) {
|
||||
system = 'macos'; // macos系统
|
||||
} else if (testUa(/x11/g)) {
|
||||
system = 'linux'; // linux系统
|
||||
} else if (testUa(/android|adr/g)) {
|
||||
system = 'android'; // android系统
|
||||
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
||||
system = 'ios'; // ios系统
|
||||
}
|
||||
|
||||
let platform = 'desktop';
|
||||
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
||||
platform = 'desktop';
|
||||
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
|
||||
platform = 'mobile';
|
||||
}
|
||||
|
||||
return platform as 'mobile' | 'desktop';
|
||||
}
|
||||
|
||||
export async function fileExist(file: any) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
fs.accessSync(file);
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export class AuthInfo {
|
||||
ip?: string;
|
||||
type: AuthDataType;
|
||||
info?: any;
|
||||
_id?: string;
|
||||
|
||||
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',
|
||||
}
|
||||
+12
-1
@@ -8,6 +8,10 @@ export class Crontab {
|
||||
_id?: string;
|
||||
status?: CrontabStatus;
|
||||
isSystem?: 1 | 0;
|
||||
pid?: number;
|
||||
isDisabled?: 1 | 0;
|
||||
log_path?: string;
|
||||
isPinned?: 1 | 0;
|
||||
|
||||
constructor(options: Crontab) {
|
||||
this.name = options.name;
|
||||
@@ -16,9 +20,15 @@ export class Crontab {
|
||||
this.saved = options.saved;
|
||||
this._id = options._id;
|
||||
this.created = options.created;
|
||||
this.status = options.status || CrontabStatus.idle;
|
||||
this.status = CrontabStatus[options.status]
|
||||
? options.status
|
||||
: CrontabStatus.idle;
|
||||
this.timestamp = new Date().toString();
|
||||
this.isSystem = options.isSystem || 0;
|
||||
this.pid = options.pid;
|
||||
this.isDisabled = options.isDisabled || 0;
|
||||
this.log_path = options.log_path || '';
|
||||
this.isPinned = options.isPinned || 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +36,5 @@ export enum CrontabStatus {
|
||||
'running',
|
||||
'idle',
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
export class Dependence {
|
||||
timestamp?: string;
|
||||
created?: number;
|
||||
_id?: string;
|
||||
status?: DependenceStatus;
|
||||
type?: DependenceTypes;
|
||||
name?: number;
|
||||
log?: string[];
|
||||
remark?: string;
|
||||
|
||||
constructor(options: Dependence) {
|
||||
this._id = options._id;
|
||||
this.created = options.created || new Date().valueOf();
|
||||
this.status = options.status || DependenceStatus.installing;
|
||||
this.type = options.type || DependenceTypes.nodejs;
|
||||
this.timestamp = new Date().toString();
|
||||
this.name = options.name;
|
||||
this.log = options.log || [];
|
||||
this.remark = options.remark || '';
|
||||
}
|
||||
}
|
||||
|
||||
export enum DependenceStatus {
|
||||
'installing',
|
||||
'installed',
|
||||
'installFailed',
|
||||
'removing',
|
||||
'removed',
|
||||
'removeFailed',
|
||||
}
|
||||
|
||||
export enum DependenceTypes {
|
||||
'nodejs',
|
||||
'python3',
|
||||
'linux',
|
||||
}
|
||||
|
||||
export enum InstallDependenceCommandTypes {
|
||||
'npm i -g --force',
|
||||
'pip3 install',
|
||||
'apk add --no-cache -f',
|
||||
}
|
||||
|
||||
export enum unInstallDependenceCommandTypes {
|
||||
'npm uninstall -g --force',
|
||||
'pip3 uninstall -y',
|
||||
'apk del -f',
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
export class Cookie {
|
||||
export class Env {
|
||||
value?: string;
|
||||
timestamp?: string;
|
||||
created?: number;
|
||||
_id?: string;
|
||||
status?: CookieStatus;
|
||||
status?: EnvStatus;
|
||||
position?: number;
|
||||
name?: number;
|
||||
remarks?: number;
|
||||
|
||||
constructor(options: Cookie) {
|
||||
constructor(options: Env) {
|
||||
this.value = options.value;
|
||||
this._id = options._id;
|
||||
this.created = options.created || new Date().valueOf();
|
||||
this.status = options.status || CookieStatus.noacquired;
|
||||
this.status = options.status || EnvStatus.normal;
|
||||
this.timestamp = new Date().toString();
|
||||
this.position = options.position;
|
||||
this.name = options.name;
|
||||
this.remarks = options.remarks;
|
||||
}
|
||||
}
|
||||
|
||||
export enum CookieStatus {
|
||||
'noacquired',
|
||||
export enum EnvStatus {
|
||||
'normal',
|
||||
'disabled',
|
||||
'invalid',
|
||||
'abnormal',
|
||||
}
|
||||
|
||||
export const initCookiePosition = 9999999999;
|
||||
export const initEnvPosition = 9999999999;
|
||||
@@ -0,0 +1,81 @@
|
||||
export enum NotificationMode {
|
||||
'goCqHttpBot' = 'goCqHttpBot',
|
||||
'serverChan' = 'serverChan',
|
||||
'bark' = 'bark',
|
||||
'telegramBot' = 'telegramBot',
|
||||
'dingtalkBot' = 'dingtalkBot',
|
||||
'weWorkBot' = 'weWorkBot',
|
||||
'weWorkApp' = 'weWorkApp',
|
||||
'iGot' = 'iGot',
|
||||
'pushPlus' = 'pushPlus',
|
||||
'email' = 'email',
|
||||
}
|
||||
|
||||
abstract class NotificationBaseInfo {
|
||||
public type!: NotificationMode;
|
||||
}
|
||||
|
||||
export class GoCqHttpBotNotification extends NotificationBaseInfo {
|
||||
public goCqHttpBotUrl = '';
|
||||
public goCqHttpBotToken = '';
|
||||
public goCqHttpBotQq = '';
|
||||
}
|
||||
|
||||
export class ServerChanNotification extends NotificationBaseInfo {
|
||||
public serverChanKey = '';
|
||||
}
|
||||
|
||||
export class BarkNotification extends NotificationBaseInfo {
|
||||
public barkPush = '';
|
||||
public barkSound = '';
|
||||
public barkGroup = 'qinglong';
|
||||
}
|
||||
|
||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||
public telegramBotToken = '';
|
||||
public telegramBotUserId = '';
|
||||
public telegramBotProxyHost = '';
|
||||
public telegramBotProxyPort = '';
|
||||
public telegramBotProxyAuth = '';
|
||||
public telegramBotApiHost = 'api.telegram.org';
|
||||
}
|
||||
|
||||
export class DingtalkBotNotification extends NotificationBaseInfo {
|
||||
public dingtalkBotToken = '';
|
||||
public dingtalkBotSecret = '';
|
||||
}
|
||||
|
||||
export class WeWorkBotNotification extends NotificationBaseInfo {
|
||||
public weWorkBotKey = '';
|
||||
}
|
||||
|
||||
export class WeWorkAppNotification extends NotificationBaseInfo {
|
||||
public weWorkAppKey = '';
|
||||
}
|
||||
|
||||
export class IGotNotification extends NotificationBaseInfo {
|
||||
public iGotPushKey = '';
|
||||
}
|
||||
|
||||
export class PushPlusNotification extends NotificationBaseInfo {
|
||||
public pushPlusToken = '';
|
||||
public pushPlusUser = '';
|
||||
}
|
||||
|
||||
export class EmailNotification extends NotificationBaseInfo {
|
||||
public emailService: string = '';
|
||||
public emailUser: string = '';
|
||||
public emailPass: string = '';
|
||||
}
|
||||
|
||||
export interface NotificationInfo
|
||||
extends GoCqHttpBotNotification,
|
||||
ServerChanNotification,
|
||||
BarkNotification,
|
||||
TelegramBotNotification,
|
||||
DingtalkBotNotification,
|
||||
WeWorkBotNotification,
|
||||
WeWorkAppNotification,
|
||||
IGotNotification,
|
||||
PushPlusNotification,
|
||||
EmailNotification {}
|
||||
@@ -0,0 +1,31 @@
|
||||
export class App {
|
||||
name: string;
|
||||
scopes: AppScope[];
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
tokens?: AppToken[];
|
||||
_id?: string;
|
||||
|
||||
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',
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export class SockMessage {
|
||||
message?: string;
|
||||
type?: SockMessageType;
|
||||
references?: string[];
|
||||
|
||||
constructor(options: SockMessage) {
|
||||
this.type = options.type;
|
||||
this.message = options.message;
|
||||
this.references = options.references;
|
||||
}
|
||||
}
|
||||
|
||||
export type SockMessageType =
|
||||
| 'ping'
|
||||
| 'installDependence'
|
||||
| 'updateSystemVersion';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare namespace Express {
|
||||
interface Request {
|
||||
platform: 'desktop' | 'mobile';
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,9 @@ import expressLoader from './express';
|
||||
import dependencyInjectorLoader from './dependencyInjector';
|
||||
import Logger from './logger';
|
||||
import initData from './initData';
|
||||
import { Application } from 'express';
|
||||
|
||||
export default async ({ expressApp }: { expressApp: any }) => {
|
||||
Logger.info('✌️ DB loaded and connected!');
|
||||
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
await dependencyInjectorLoader({
|
||||
models: [],
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import DataStore from 'nedb';
|
||||
import config from '../config';
|
||||
import Logger from './logger';
|
||||
import fs from 'fs';
|
||||
import { fileExist } from '../config/util';
|
||||
|
||||
interface Dbs {
|
||||
cronDb: DataStore;
|
||||
dependenceDb: DataStore;
|
||||
envDb: DataStore;
|
||||
appDb: DataStore;
|
||||
authDb: DataStore;
|
||||
}
|
||||
|
||||
const db: Dbs = {} as any;
|
||||
|
||||
async function truncateDb() {
|
||||
return new Promise(async (resolve) => {
|
||||
const files = [
|
||||
config.cronDbFile,
|
||||
config.dependenceDbFile,
|
||||
config.envDbFile,
|
||||
config.appDbFile,
|
||||
config.authDbFile,
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
const _fileExist = await fileExist(file);
|
||||
if (_fileExist && fs.statSync(file).size >= 1024 * 1024 * 500) {
|
||||
fs.truncateSync(file, 1024 * 1024 * 500);
|
||||
}
|
||||
}
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
export default async () => {
|
||||
try {
|
||||
await truncateDb();
|
||||
|
||||
db.cronDb = new DataStore({ filename: config.cronDbFile, autoload: true });
|
||||
db.dependenceDb = new DataStore({
|
||||
filename: config.dependenceDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
db.envDb = new DataStore({ filename: config.envDbFile, autoload: true });
|
||||
db.appDb = new DataStore({ filename: config.appDbFile, autoload: true });
|
||||
db.authDb = new DataStore({ filename: config.authDbFile, autoload: true });
|
||||
|
||||
// compaction data file
|
||||
db.cronDb.persistence.compactDatafile();
|
||||
db.envDb.persistence.compactDatafile();
|
||||
db.dependenceDb.persistence.compactDatafile();
|
||||
db.appDb.persistence.compactDatafile();
|
||||
db.authDb.persistence.compactDatafile();
|
||||
|
||||
Logger.info('✌️ DB loaded');
|
||||
} catch (error) {
|
||||
Logger.info('✌️ DB load failed');
|
||||
Logger.info(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const dbs: Dbs = db;
|
||||
+107
-9
@@ -5,34 +5,124 @@ import routes from '../api';
|
||||
import config from '../config';
|
||||
import jwt from 'express-jwt';
|
||||
import fs from 'fs';
|
||||
import { getFileContentByName, 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';
|
||||
|
||||
export default ({ app }: { app: Application }) => {
|
||||
app.enable('trust proxy');
|
||||
app.use(cors());
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
||||
next();
|
||||
} else {
|
||||
return handler(req, res, {
|
||||
public: 'dist',
|
||||
rewrites: [{ source: '**', destination: '/index.html' }],
|
||||
});
|
||||
}
|
||||
});
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||
|
||||
app.use(
|
||||
jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({
|
||||
path: ['/api/login'],
|
||||
jwt({
|
||||
secret: config.secret as string,
|
||||
algorithms: ['HS384'],
|
||||
}).unless({
|
||||
path: [...config.apiWhiteList, /^\/open\//],
|
||||
}),
|
||||
);
|
||||
app.use((req, res, next) => {
|
||||
if (req.url && req.url.includes('/api/login')) {
|
||||
|
||||
app.use((req: Request, res, next) => {
|
||||
if (!req.headers) {
|
||||
req.platform = 'desktop';
|
||||
} else {
|
||||
const platform = getPlatform(req.headers['user-agent'] || '');
|
||||
req.platform = platform;
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
const headerToken = getToken(req);
|
||||
if (req.path.startsWith('/open/')) {
|
||||
const openService = Container.get(OpenService);
|
||||
const doc = await openService.findTokenByValue(headerToken);
|
||||
if (doc && doc.tokens && doc.tokens.length > 0) {
|
||||
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
||||
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
||||
const key = keyMatch && keyMatch[1];
|
||||
if (
|
||||
doc.scopes.includes(key as any) &&
|
||||
currentToken &&
|
||||
currentToken.expiration >= Math.round(Date.now() / 1000)
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!headerToken &&
|
||||
req.path &&
|
||||
config.apiWhiteList.includes(req.path) &&
|
||||
req.path !== '/api/crons/status'
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
const remoteAddress = req.socket.remoteAddress;
|
||||
if (
|
||||
remoteAddress === '::ffff:127.0.0.1' &&
|
||||
req.path === '/api/crons/status'
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
const authHeader = req.headers.authorization;
|
||||
if (data) {
|
||||
const { token } = JSON.parse(data);
|
||||
if (token && authHeader.includes(token)) {
|
||||
const { token = '', tokens = {} } = JSON.parse(data);
|
||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
const err: any = new Error('UnauthorizedError');
|
||||
err['status'] = 401;
|
||||
err.status = 401;
|
||||
next(err);
|
||||
});
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
if (!['/api/init/user', '/api/init/notification'].includes(req.path)) {
|
||||
return next();
|
||||
}
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const envDbContent = getFileContentByName(config.envDbFile);
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
Object.keys(authInfo).length === 2 &&
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'admin' &&
|
||||
envDbContent.length === 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((req, res, next) => {
|
||||
@@ -41,6 +131,8 @@ export default ({ app }: { app: Application }) => {
|
||||
next(err);
|
||||
});
|
||||
|
||||
app.use(Sentry.Handlers.errorHandler());
|
||||
|
||||
app.use(
|
||||
(
|
||||
err: Error & { status: number },
|
||||
@@ -49,7 +141,10 @@ export default ({ app }: { app: Application }) => {
|
||||
next: NextFunction,
|
||||
) => {
|
||||
if (err.name === 'UnauthorizedError') {
|
||||
return res.status(err.status).send({ message: err.message }).end();
|
||||
return res
|
||||
.status(err.status)
|
||||
.send({ code: 401, message: err.message })
|
||||
.end();
|
||||
}
|
||||
return next(err);
|
||||
},
|
||||
@@ -62,8 +157,11 @@ export default ({ app }: { app: Application }) => {
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
Sentry.captureException(err);
|
||||
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
code: err.status || 500,
|
||||
message: err.message,
|
||||
});
|
||||
},
|
||||
|
||||
+65
-73
@@ -1,89 +1,81 @@
|
||||
import DependenceService from '../services/dependence';
|
||||
import { exec } from 'child_process';
|
||||
import { Container } from 'typedi';
|
||||
import { Crontab, CrontabStatus } from '../data/cron';
|
||||
import CronService from '../services/cron';
|
||||
|
||||
const initData = [
|
||||
{
|
||||
name: '更新面板',
|
||||
command: `sleep ${randomSchedule(
|
||||
60,
|
||||
1,
|
||||
)} && git_pull >> $QL_DIR/log/git_pull.log 2>&1`,
|
||||
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
|
||||
24,
|
||||
7,
|
||||
).toString()} * * *`,
|
||||
status: CrontabStatus.idle,
|
||||
},
|
||||
{
|
||||
name: 'build面板',
|
||||
command: 'rebuild >> ${QL_DIR}/log/rebuild.log 2>&1',
|
||||
schedule: '30 7 */7 * *',
|
||||
status: CrontabStatus.disabled,
|
||||
},
|
||||
{
|
||||
name: '自定义仓库',
|
||||
command: `sleep ${randomSchedule(
|
||||
60,
|
||||
1,
|
||||
)} && diy https://ghproxy.com/https://github.com/whyour/hundun.git "quanx/jx|quanx/jd" tokens >> $QL_DIR/log/diy_pull.log 2>&1`,
|
||||
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
|
||||
24,
|
||||
6,
|
||||
).toString()} * * *`,
|
||||
status: CrontabStatus.idle,
|
||||
},
|
||||
{
|
||||
name: '互助码导出',
|
||||
command: 'export_sharecodes',
|
||||
schedule: '48 5 * * *',
|
||||
status: CrontabStatus.idle,
|
||||
},
|
||||
{
|
||||
name: '删除日志',
|
||||
command: 'rm_log >/dev/null 2>&1',
|
||||
schedule: '30 7 */7 * *',
|
||||
status: CrontabStatus.disabled,
|
||||
},
|
||||
{
|
||||
name: '重置密码',
|
||||
command: 'js resetpwd',
|
||||
schedule: '33 6 */7 * *',
|
||||
status: CrontabStatus.disabled,
|
||||
},
|
||||
{
|
||||
name: '运行所有脚本(慎用)',
|
||||
command: 'js runall',
|
||||
schedule: '33 6 */7 * *',
|
||||
status: CrontabStatus.disabled,
|
||||
},
|
||||
];
|
||||
import EnvService from '../services/env';
|
||||
import _ from 'lodash';
|
||||
import { dbs } from '../loaders/db';
|
||||
|
||||
export default async () => {
|
||||
const cronService = Container.get(CronService);
|
||||
const cronDb = cronService.getDb();
|
||||
const envService = Container.get(EnvService);
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const cronDb = dbs.cronDb;
|
||||
const dependenceDb = dbs.dependenceDb;
|
||||
|
||||
cronDb.count({}, async (err, count) => {
|
||||
const data = initData.map((x) => {
|
||||
const tab = new Crontab(x);
|
||||
tab.created = new Date().valueOf();
|
||||
tab.saved = false;
|
||||
if (tab.name === '更新面板') {
|
||||
tab.isSystem = 1;
|
||||
} else {
|
||||
tab.isSystem = 0;
|
||||
// 初始化更新所有任务状态为空闲
|
||||
cronDb.update(
|
||||
{ status: { $in: [CrontabStatus.running, CrontabStatus.queued] } },
|
||||
{ $set: { status: CrontabStatus.idle } },
|
||||
{ multi: true },
|
||||
);
|
||||
|
||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||
dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec((err, docs) => {
|
||||
const groups = _.groupBy(docs, 'type');
|
||||
for (const key in groups) {
|
||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||
const group = groups[key];
|
||||
const depIds = group.map((x) => x._id);
|
||||
for (const dep of depIds) {
|
||||
dependenceService.reInstall([dep]);
|
||||
}
|
||||
}
|
||||
return tab;
|
||||
});
|
||||
if (count === 0) {
|
||||
cronDb.insert(data);
|
||||
await cronService.autosave_crontab();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化时执行一次所有的ql repo 任务
|
||||
cronDb
|
||||
.find({
|
||||
command: /ql (repo|raw)/,
|
||||
isDisabled: { $ne: 1 },
|
||||
})
|
||||
.exec((err, docs) => {
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
if (doc) {
|
||||
exec(doc.command);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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和定时任务数据
|
||||
await cronService.autosave_crontab();
|
||||
await envService.set_envs();
|
||||
};
|
||||
|
||||
function randomSchedule(from: number, to: number) {
|
||||
const result = [];
|
||||
const result: any[] = [];
|
||||
const arr = [...Array(from).keys()];
|
||||
let count = arr.length;
|
||||
for (let i = 0; i < to; i++) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Application } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import * as Tracing from '@sentry/tracing';
|
||||
import Logger from './logger';
|
||||
|
||||
export default ({ expressApp }: { expressApp: Application }) => {
|
||||
Sentry.init({
|
||||
dsn: 'https://e14681bce55f4849b11024a7d424b711@o1051273.ingest.sentry.io/6047906',
|
||||
integrations: [
|
||||
new Sentry.Integrations.Http({ tracing: true }),
|
||||
new Tracing.Integrations.Express({ app: expressApp }),
|
||||
],
|
||||
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
expressApp.use(Sentry.Handlers.requestHandler());
|
||||
expressApp.use(Sentry.Handlers.tracingHandler());
|
||||
|
||||
Logger.info('✌️ Sentry loaded');
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Server } from 'http';
|
||||
import Logger from './logger';
|
||||
import Sock from './sock';
|
||||
|
||||
export default async ({ server }: { server: Server }) => {
|
||||
await Sock({ server });
|
||||
Logger.info('✌️ Sock loaded');
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
Logger.info('✌️ Server need close');
|
||||
server.close(() => {
|
||||
Logger.info('✌️ Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import sockJs from 'sockjs';
|
||||
import { Server } from 'http';
|
||||
import Logger from './logger';
|
||||
import { Container } from 'typedi';
|
||||
import SockService from '../services/sock';
|
||||
import config from '../config/index';
|
||||
import fs from 'fs';
|
||||
import { getPlatform } from '../config/util';
|
||||
|
||||
export default async ({ server }: { server: Server }) => {
|
||||
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||
const sockService = Container.get(SockService);
|
||||
|
||||
echo.on('connection', (conn) => {
|
||||
if (!conn.headers || !conn.url || !conn.pathname) {
|
||||
conn.close('404');
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||
if (data) {
|
||||
const { token = '', tokens = {} } = JSON.parse(data);
|
||||
if (headerToken === token || tokens[platform] === headerToken) {
|
||||
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
||||
sockService.addClient(conn);
|
||||
|
||||
conn.on('data', (message) => {
|
||||
conn.write(message);
|
||||
});
|
||||
|
||||
conn.on('close', function () {
|
||||
sockService.removeClient(conn);
|
||||
});
|
||||
|
||||
return;
|
||||
} else {
|
||||
conn.write(JSON.stringify({ type: 'ping', message: 'whyour' }));
|
||||
}
|
||||
}
|
||||
|
||||
conn.close('404');
|
||||
});
|
||||
|
||||
echo.installHandlers(server);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import schedule from 'node-schedule';
|
||||
import express from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import Logger from './loaders/logger';
|
||||
import { CrontabStatus } from './data/cron';
|
||||
import config from './config';
|
||||
import { dbs } from './loaders/db';
|
||||
|
||||
const app = express();
|
||||
|
||||
const run = async () => {
|
||||
const cronDb = dbs.cronDb;
|
||||
|
||||
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, async () => {
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
await require('./loaders/db').default();
|
||||
|
||||
await run();
|
||||
Logger.info(`
|
||||
################################################
|
||||
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
||||
################################################
|
||||
`);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,281 +0,0 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import fetch from 'node-fetch';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import got from 'got';
|
||||
import DataStore from 'nedb';
|
||||
import { Cookie, CookieStatus, initCookiePosition } from '../data/cookie';
|
||||
|
||||
@Service()
|
||||
export default class CookieService {
|
||||
private cronDb = new DataStore({ filename: config.cookieDbFile });
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
||||
this.cronDb.loadDatabase((err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
}
|
||||
|
||||
public async getCookies() {
|
||||
const content = getFileContentByName(config.cookieFile);
|
||||
return this.formatCookie(content.split('\n').filter((x) => !!x));
|
||||
}
|
||||
|
||||
public async addCookie(cookies: string[]) {
|
||||
let content = getFileContentByName(config.cookieFile);
|
||||
const originCookies = content.split('\n').filter((x) => !!x);
|
||||
const result = originCookies.concat(cookies);
|
||||
fs.writeFileSync(config.cookieFile, result.join('\n'));
|
||||
return '';
|
||||
}
|
||||
|
||||
public async updateCookie({ cookie, oldCookie }) {
|
||||
let content = getFileContentByName(config.cookieFile);
|
||||
const cookies = content.split('\n');
|
||||
const index = cookies.findIndex((x) => x === oldCookie);
|
||||
if (index !== -1) {
|
||||
cookies[index] = cookie;
|
||||
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
|
||||
return '';
|
||||
} else {
|
||||
return '未找到要原有Cookie';
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCookie(cookie: string) {
|
||||
let content = getFileContentByName(config.cookieFile);
|
||||
const cookies = content.split('\n');
|
||||
const index = cookies.findIndex((x) => x === cookie);
|
||||
if (index !== -1) {
|
||||
cookies.splice(index, 1);
|
||||
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
|
||||
return '';
|
||||
} else {
|
||||
return '未找到要删除的Cookie';
|
||||
}
|
||||
}
|
||||
|
||||
private async formatCookie(data: any[]) {
|
||||
const result = [];
|
||||
for (const x of data) {
|
||||
const { nickname, status } = await this.getJdInfo(x);
|
||||
if (/pt_pin=(.+?);/.test(x)) {
|
||||
result.push({
|
||||
pin: x.match(/pt_pin=(.+?);/)[1],
|
||||
cookie: x,
|
||||
status,
|
||||
nickname: nickname,
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
pin: 'pin未匹配到',
|
||||
cookie: x,
|
||||
status,
|
||||
nickname: nickname,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async refreshCookie(_id: string) {
|
||||
const current = await this.get(_id);
|
||||
const { status, nickname } = await this.getJdInfo(current.value);
|
||||
return {
|
||||
...current,
|
||||
status,
|
||||
nickname,
|
||||
};
|
||||
}
|
||||
|
||||
private getJdInfo(cookie: string) {
|
||||
return fetch(
|
||||
`https://me-api.jd.com/user_new/info/GetJDUserInfoUnion?orgFlag=JD_PinGou_New&callSource=mainorder&channel=4&isHomewhite=0&sceneval=2&_=${Date.now()}&sceneval=2&g_login_type=1&g_ty=ls`,
|
||||
{
|
||||
method: 'get',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Accept-Language': 'zh-cn',
|
||||
Connection: 'keep-alive',
|
||||
Cookie: cookie,
|
||||
Referer: 'https://home.m.jd.com/myJd/newhome.action',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1',
|
||||
Host: 'me-api.jd.com',
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => {
|
||||
if (x.retcode === '0' && x.data && x.data.userInfo) {
|
||||
return {
|
||||
nickname: x.data.userInfo.baseInfo.nickname,
|
||||
status: CookieStatus.normal,
|
||||
};
|
||||
} else if (x.retcode === 13) {
|
||||
return { status: CookieStatus.invalid, nickname: '-' };
|
||||
}
|
||||
return { status: CookieStatus.abnormal, nickname: '-' };
|
||||
});
|
||||
}
|
||||
|
||||
public async create(payload: string[]): Promise<Cookie[]> {
|
||||
const cookies = await this.cookies('');
|
||||
let position = initCookiePosition;
|
||||
if (cookies && cookies.length > 0) {
|
||||
position = cookies[cookies.length - 1].position;
|
||||
}
|
||||
const tabs = payload.map((x) => {
|
||||
const cookie = new Cookie({ value: x, position });
|
||||
position = position / 2;
|
||||
cookie.position = position;
|
||||
return cookie;
|
||||
});
|
||||
const docs = await this.insert(tabs);
|
||||
await this.set_cookies();
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async insert(payload: Cookie[]): Promise<Cookie[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb.insert(payload, (err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async update(payload: Cookie): Promise<Cookie> {
|
||||
const { _id, ...other } = payload;
|
||||
const doc = await this.get(_id);
|
||||
const tab = new Cookie({ ...doc, ...other });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
await this.set_cookies();
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
public async updateDb(payload: Cookie): Promise<Cookie> {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb.update(
|
||||
{ _id: payload._id },
|
||||
payload,
|
||||
{ returnUpdatedDocs: true },
|
||||
(err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs as Cookie);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async remove(_id: string) {
|
||||
this.cronDb.remove({ _id }, {});
|
||||
await this.set_cookies();
|
||||
}
|
||||
|
||||
public async move(
|
||||
_id: string,
|
||||
{
|
||||
fromIndex,
|
||||
toIndex,
|
||||
}: {
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
},
|
||||
) {
|
||||
let targetPosition: number;
|
||||
const isUpward = fromIndex > toIndex;
|
||||
const cookies = await this.cookies();
|
||||
if (toIndex === 0 || toIndex === cookies.length - 1) {
|
||||
targetPosition = isUpward
|
||||
? cookies[0].position * 2
|
||||
: cookies[toIndex].position / 2;
|
||||
} else {
|
||||
targetPosition = isUpward
|
||||
? (cookies[toIndex].position + cookies[toIndex - 1].position) / 2
|
||||
: (cookies[toIndex].position + cookies[toIndex + 1].position) / 2;
|
||||
}
|
||||
this.update({
|
||||
_id,
|
||||
position: targetPosition,
|
||||
});
|
||||
await this.set_cookies();
|
||||
}
|
||||
|
||||
public async cookies(
|
||||
searchText?: string,
|
||||
sort: any = { position: -1 },
|
||||
): Promise<Cookie[]> {
|
||||
let query = {};
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText);
|
||||
query = {
|
||||
$or: [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
command: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb
|
||||
.find(query)
|
||||
.sort({ ...sort })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async get(_id: string): Promise<Cookie> {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb.find({ _id }).exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async getBySort(sort: any): Promise<Cookie> {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb
|
||||
.find({})
|
||||
.sort({ ...sort })
|
||||
.limit(1)
|
||||
.exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async disabled(_id: string) {
|
||||
this.cronDb.update({ _id }, { $set: { status: CookieStatus.disabled } });
|
||||
await this.set_cookies();
|
||||
}
|
||||
|
||||
public async enabled(_id: string) {
|
||||
this.cronDb.update({ _id }, { $set: { status: CookieStatus.noacquired } });
|
||||
}
|
||||
|
||||
private async set_cookies() {
|
||||
const cookies = await this.cookies();
|
||||
let cookie_string = '';
|
||||
cookies.forEach((tab) => {
|
||||
if (tab.status !== CookieStatus.disabled) {
|
||||
cookie_string += tab.value;
|
||||
cookie_string += '\n';
|
||||
}
|
||||
});
|
||||
fs.writeFileSync(config.cookieFile, cookie_string);
|
||||
}
|
||||
}
|
||||
+288
-78
@@ -7,19 +7,27 @@ import { exec, execSync, spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
import PQueue from 'p-queue';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { dbs } from '../loaders/db';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
private cronDb = new DataStore({ filename: config.cronDbFile });
|
||||
private cronDb = dbs.cronDb;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
||||
this.cronDb.loadDatabase((err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
}
|
||||
private queue = new PQueue({
|
||||
concurrency: parseInt(process.env.MaxConcurrentNum as string) || 5,
|
||||
});
|
||||
|
||||
public getDb(): DataStore {
|
||||
return this.cronDb;
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
private isSixCron(cron: Crontab) {
|
||||
const { schedule } = cron;
|
||||
if (schedule.split(/ +/).length === 6) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async create(payload: Crontab): Promise<Crontab> {
|
||||
@@ -27,7 +35,7 @@ export default class CronService {
|
||||
tab.created = new Date().valueOf();
|
||||
tab.saved = false;
|
||||
const doc = await this.insert(tab);
|
||||
await this.set_crontab();
|
||||
await this.set_crontab(this.isSixCron(doc));
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -49,7 +57,7 @@ export default class CronService {
|
||||
const tab = new Crontab({ ...doc, ...other });
|
||||
tab.saved = false;
|
||||
const newDoc = await this.updateDb(tab);
|
||||
await this.set_crontab();
|
||||
await this.set_crontab(this.isSixCron(newDoc));
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
@@ -70,19 +78,84 @@ export default class CronService {
|
||||
});
|
||||
}
|
||||
|
||||
public async status(_id: string, stopped: boolean) {
|
||||
this.cronDb.update({ _id }, { $set: { stopped, saved: false } });
|
||||
public async status({
|
||||
ids,
|
||||
status,
|
||||
pid,
|
||||
log_path,
|
||||
last_running_time = 0,
|
||||
last_execution_time = 0,
|
||||
}: {
|
||||
ids: string[];
|
||||
status: CrontabStatus;
|
||||
pid: number;
|
||||
log_path: string;
|
||||
last_running_time: number;
|
||||
last_execution_time: number;
|
||||
}) {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{
|
||||
$set: {
|
||||
status,
|
||||
pid,
|
||||
log_path,
|
||||
last_running_time,
|
||||
last_execution_time,
|
||||
},
|
||||
},
|
||||
{ multi: true, returnUpdatedDocs: true },
|
||||
(err) => {
|
||||
resolve(null);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async remove(_id: string) {
|
||||
this.cronDb.remove({ _id }, {});
|
||||
await this.set_crontab();
|
||||
public async remove(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.remove(
|
||||
{ _id: { $in: ids } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
await this.set_crontab(true);
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async pin(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { isPinned: 1 } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async unPin(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { isPinned: 0 } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
||||
let query = {};
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText);
|
||||
const reg = new RegExp(searchText, 'i');
|
||||
query = {
|
||||
$or: [
|
||||
{
|
||||
@@ -91,13 +164,16 @@ export default class CronService {
|
||||
{
|
||||
command: reg,
|
||||
},
|
||||
{
|
||||
schedule: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb
|
||||
.find(query)
|
||||
.sort({ created: 1 })
|
||||
.sort({ created: -1 })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
@@ -112,73 +188,206 @@ export default class CronService {
|
||||
});
|
||||
}
|
||||
|
||||
public async run(_id: string) {
|
||||
this.cronDb.find({ _id }).exec((err, docs: Crontab[]) => {
|
||||
let res = docs[0];
|
||||
public async run(ids: string[]) {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: CrontabStatus.queued } },
|
||||
{ multi: true },
|
||||
);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
this.queue.add(() => this.runSingle(id));
|
||||
}
|
||||
}
|
||||
|
||||
public async stop(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb
|
||||
.find({ _id: { $in: ids } })
|
||||
.exec(async (err, docs: Crontab[]) => {
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
try {
|
||||
process.kill(-doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.silly(error);
|
||||
}
|
||||
}
|
||||
const err = await this.killTask(doc.command);
|
||||
if (doc.log_path) {
|
||||
const str = err ? `\n${err}` : '';
|
||||
fs.appendFileSync(
|
||||
`${doc.log_path}`,
|
||||
`${str}\n## 执行结束... ${new Date()
|
||||
.toLocaleString('zh', { hour12: false })
|
||||
.replace(' 24:', ' 00:')} `,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
||||
{ multi: true },
|
||||
);
|
||||
this.queue.clear();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async killTask(name: string) {
|
||||
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
||||
const execAsync = promisify(exec);
|
||||
try {
|
||||
let pid = (await execAsync(taskCommand)).stdout;
|
||||
if (pid) {
|
||||
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const pids = pid.match(/\(\d+/g);
|
||||
const killLogs = [];
|
||||
for (const id of pids) {
|
||||
const c = `kill -9 ${id.slice(1)}`;
|
||||
const { stdout, stderr } = await execAsync(c);
|
||||
if (stderr) {
|
||||
killLogs.push(stderr);
|
||||
}
|
||||
if (stdout) {
|
||||
killLogs.push(stdout);
|
||||
}
|
||||
}
|
||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
||||
} catch (e) {
|
||||
return JSON.stringify(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async runSingle(id: string): Promise<number> {
|
||||
return new Promise(async (resolve: any) => {
|
||||
const cron = await this.get(id);
|
||||
if (cron.status !== CrontabStatus.queued) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
let { _id, command, log_path } = cron;
|
||||
|
||||
this.logger.silly('Running job');
|
||||
this.logger.silly('ID: ' + _id);
|
||||
this.logger.silly('Original command: ' + res.command);
|
||||
this.logger.silly('Original command: ' + command);
|
||||
|
||||
let logFile = `${config.manualLogPath}${res._id}.log`;
|
||||
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`);
|
||||
|
||||
let cmdStr = res.command;
|
||||
if (res.command.startsWith('js') && !res.command.endsWith('now')) {
|
||||
cmdStr = `${res.command} now`;
|
||||
} else if (/&& (.*) >>/.test(res.command)) {
|
||||
cmdStr = res.command.match(/&& (.*) >>/)[1];
|
||||
let cmdStr = command;
|
||||
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
||||
cmdStr = `task ${cmdStr}`;
|
||||
}
|
||||
if (cmdStr.endsWith('.js')) {
|
||||
cmdStr = `${cmdStr} now`;
|
||||
}
|
||||
const cmd = spawn(cmdStr, { shell: true });
|
||||
|
||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.running } });
|
||||
|
||||
cmd.stdout.on('data', (data) => {
|
||||
this.logger.silly(`stdout: ${data}`);
|
||||
fs.appendFileSync(logFile, data);
|
||||
const cp = spawn(cmdStr, { shell: '/bin/bash' });
|
||||
this.cronDb.update(
|
||||
{ _id },
|
||||
{ $set: { status: CrontabStatus.running, pid: cp.pid } },
|
||||
);
|
||||
cp.stderr.on('data', (data) => {
|
||||
if (log_path) {
|
||||
fs.appendFileSync(`${log_path}`, `${data}`);
|
||||
}
|
||||
});
|
||||
cp.on('error', (err) => {
|
||||
if (log_path) {
|
||||
fs.appendFileSync(`${log_path}`, `${JSON.stringify(err)}`);
|
||||
}
|
||||
});
|
||||
|
||||
cmd.stderr.on('data', (data) => {
|
||||
this.logger.error(`stderr: ${data}`);
|
||||
fs.appendFileSync(logFile, data);
|
||||
cp.on('exit', (code, signal) => {
|
||||
this.logger.info(
|
||||
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||
);
|
||||
this.cronDb.update(
|
||||
{ _id },
|
||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
|
||||
cmd.on('close', (code) => {
|
||||
this.logger.silly(`child process exited with code ${code}`);
|
||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
||||
});
|
||||
|
||||
cmd.on('error', (err) => {
|
||||
this.logger.silly(err);
|
||||
fs.appendFileSync(logFile, err.stack);
|
||||
});
|
||||
|
||||
cmd.on('exit', (code: number, signal: any) => {
|
||||
this.logger.silly(`cmd exit ${code}`);
|
||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
||||
fs.appendFileSync(logFile, `\n\n执行结束...`);
|
||||
});
|
||||
|
||||
cmd.on('disconnect', () => {
|
||||
this.logger.silly(`cmd disconnect`);
|
||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
||||
fs.appendFileSync(logFile, `\n\n连接断开...`);
|
||||
cp.on('close', (code) => {
|
||||
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||
this.cronDb.update(
|
||||
{ _id },
|
||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async disabled(_id: string) {
|
||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.disabled } });
|
||||
await this.set_crontab();
|
||||
public async disabled(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { isDisabled: 1 } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
await this.set_crontab(true);
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async enabled(_id: string) {
|
||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
||||
public async enabled(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { isDisabled: 0 } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
await this.set_crontab(true);
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async log(_id: string) {
|
||||
let logFile = `${config.manualLogPath}${_id}.log`;
|
||||
return getFileContentByName(logFile);
|
||||
const doc = await this.get(_id);
|
||||
if (!doc) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (doc.log_path) {
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
private getKey(command: string) {
|
||||
const start =
|
||||
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
||||
const end =
|
||||
command.lastIndexOf('.') !== -1
|
||||
? command.lastIndexOf('.')
|
||||
: command.length;
|
||||
return command.substring(start, end);
|
||||
}
|
||||
|
||||
private make_command(tab: Crontab) {
|
||||
@@ -186,11 +395,12 @@ export default class CronService {
|
||||
return crontab_job_string;
|
||||
}
|
||||
|
||||
private async set_crontab() {
|
||||
private async set_crontab(needReloadSchedule: boolean = false) {
|
||||
const tabs = await this.crontabs();
|
||||
var crontab_string = '';
|
||||
tabs.forEach((tab) => {
|
||||
if (tab.status === CrontabStatus.disabled) {
|
||||
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||
if (tab.isDisabled === 1 || _schedule.length !== 5) {
|
||||
crontab_string += '# ';
|
||||
crontab_string += tab.schedule;
|
||||
crontab_string += ' ';
|
||||
@@ -208,13 +418,12 @@ export default class CronService {
|
||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||
|
||||
execSync(`crontab ${config.crontabFile}`);
|
||||
if (needReloadSchedule) {
|
||||
exec(`pm2 reload schedule`);
|
||||
}
|
||||
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
|
||||
}
|
||||
|
||||
private reload_db() {
|
||||
this.cronDb.loadDatabase();
|
||||
}
|
||||
|
||||
public import_crontab() {
|
||||
exec('crontab -l', (error, stdout, stderr) => {
|
||||
var lines = stdout.split('\n');
|
||||
@@ -222,15 +431,16 @@ export default class CronService {
|
||||
|
||||
lines.reverse().forEach((line, index) => {
|
||||
line = line.replace(/\t+/g, ' ');
|
||||
var regex = /^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
|
||||
var regex =
|
||||
/^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
|
||||
var command = line.replace(regex, '').trim();
|
||||
var schedule = line.replace(command, '').trim();
|
||||
|
||||
var is_valid = false;
|
||||
try {
|
||||
is_valid = cron_parser.parseString(line).expressions.length > 0;
|
||||
} catch (e) {}
|
||||
if (command && schedule && is_valid) {
|
||||
if (
|
||||
command &&
|
||||
schedule &&
|
||||
cron_parser.parseExpression(schedule).hasNext()
|
||||
) {
|
||||
var name = namePrefix + '_' + index;
|
||||
|
||||
this.cronDb.findOne({ command, schedule }, (err, doc) => {
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import DataStore from 'nedb';
|
||||
import {
|
||||
Dependence,
|
||||
InstallDependenceCommandTypes,
|
||||
DependenceStatus,
|
||||
DependenceTypes,
|
||||
unInstallDependenceCommandTypes,
|
||||
} from '../data/dependence';
|
||||
import _ from 'lodash';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import { dbs } from '../loaders/db';
|
||||
|
||||
@Service()
|
||||
export default class DependenceService {
|
||||
private dependenceDb = dbs.dependenceDb;
|
||||
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private sockService: SockService,
|
||||
) {}
|
||||
|
||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||
const tabs = payloads.map((x) => {
|
||||
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
|
||||
return tab;
|
||||
});
|
||||
const docs = await this.insert(tabs);
|
||||
this.installOrUninstallDependencies(docs);
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb.insert(payloads, (err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async update(
|
||||
payload: Dependence & { _id: string },
|
||||
): Promise<Dependence> {
|
||||
const { _id, ...other } = payload;
|
||||
const doc = await this.get(_id);
|
||||
const tab = new Dependence({
|
||||
...doc,
|
||||
...other,
|
||||
status: DependenceStatus.installing,
|
||||
});
|
||||
const newDoc = await this.updateDb(tab);
|
||||
this.installOrUninstallDependencies([newDoc]);
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
private async updateDb(payload: Dependence): Promise<Dependence> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb.update(
|
||||
{ _id: payload._id },
|
||||
payload,
|
||||
{ returnUpdatedDocs: true },
|
||||
(err, num, doc) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(doc as Dependence);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async remove(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.dependenceDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: DependenceStatus.removing, log: [] } },
|
||||
{ multi: true, returnUpdatedDocs: true },
|
||||
async (err, num, docs: Dependence[]) => {
|
||||
this.installOrUninstallDependencies(docs, false);
|
||||
resolve(docs);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async removeDb(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.dependenceDb.remove(
|
||||
{ _id: { $in: ids } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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 reg = new RegExp(searchValue);
|
||||
condition = {
|
||||
...condition,
|
||||
$or: [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
const newDocs = await this.find(condition, sort);
|
||||
return newDocs;
|
||||
}
|
||||
|
||||
public async reInstall(ids: string[]): Promise<Dependence[]> {
|
||||
return new Promise((resolve: any) => {
|
||||
this.dependenceDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: DependenceStatus.installing, log: [] } },
|
||||
{ multi: true, returnUpdatedDocs: true },
|
||||
async (err, num, docs: Dependence[]) => {
|
||||
this.installOrUninstallDependencies(docs);
|
||||
resolve(docs);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async find(query: any, sort: any): Promise<Dependence[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb
|
||||
.find(query)
|
||||
.sort({ ...sort })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async get(_id: string): Promise<Dependence> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb.find({ _id }).exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async updateLog(ids: string[], log: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $push: { log } },
|
||||
{ multi: true },
|
||||
(err, num, doc) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public installOrUninstallDependencies(
|
||||
dependencies: Dependence[],
|
||||
isInstall: boolean = true,
|
||||
) {
|
||||
if (dependencies.length === 0) {
|
||||
return;
|
||||
}
|
||||
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 string[];
|
||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||
const startTime = Date.now();
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).toLocaleString()}`,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(
|
||||
depIds,
|
||||
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).toLocaleString()}\n`,
|
||||
);
|
||||
cp.stdout.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.on('error', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: JSON.stringify(err),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('close', (code) => {
|
||||
const endTime = Date.now();
|
||||
const isSucceed = code === 0;
|
||||
const resultText = isSucceed ? '成功' : '失败';
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: `依赖${actionText}${resultText},结束时间 ${new Date(
|
||||
endTime,
|
||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(
|
||||
depIds,
|
||||
`依赖${actionText}${resultText},结束时间 ${new Date(
|
||||
endTime,
|
||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||
);
|
||||
|
||||
let status = null;
|
||||
if (isSucceed) {
|
||||
status = isInstall
|
||||
? DependenceStatus.installed
|
||||
: DependenceStatus.removed;
|
||||
} else {
|
||||
status = isInstall
|
||||
? DependenceStatus.installFailed
|
||||
: DependenceStatus.removeFailed;
|
||||
}
|
||||
this.dependenceDb.update(
|
||||
{ _id: { $in: depIds } },
|
||||
{
|
||||
$set: { status },
|
||||
$unset: { pid: true },
|
||||
},
|
||||
{ multi: true },
|
||||
);
|
||||
|
||||
// 如果删除依赖成功,3秒后删除数据库记录
|
||||
if (isSucceed && !isInstall) {
|
||||
setTimeout(() => {
|
||||
this.removeDb(depIds);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import DataStore from 'nedb';
|
||||
import { Env, EnvStatus, initEnvPosition } from '../data/env';
|
||||
import _ from 'lodash';
|
||||
import { dbs } from '../loaders/db';
|
||||
|
||||
@Service()
|
||||
export default class EnvService {
|
||||
private envDb = dbs.envDb;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public async create(payloads: Env[]): Promise<Env[]> {
|
||||
const envs = await this.envs();
|
||||
let position = initEnvPosition;
|
||||
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
|
||||
position = envs[envs.length - 1].position as number;
|
||||
}
|
||||
const tabs = payloads.map((x) => {
|
||||
position = position / 2;
|
||||
const tab = new Env({ ...x, position });
|
||||
return tab;
|
||||
});
|
||||
const docs = await this.insert(tabs);
|
||||
await this.set_envs();
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async insert(payloads: Env[]): Promise<Env[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb.insert(payloads, (err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async update(payload: Env): Promise<Env> {
|
||||
const { _id, ...other } = payload;
|
||||
const doc = await this.get(_id);
|
||||
const tab = new Env({ ...doc, ...other });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
await this.set_envs();
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
private async updateDb(payload: Env): Promise<Env> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb.update(
|
||||
{ _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[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.envDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
|
||||
await this.set_envs();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async move(
|
||||
_id: string,
|
||||
{
|
||||
fromIndex,
|
||||
toIndex,
|
||||
}: {
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
},
|
||||
): Promise<Env> {
|
||||
let targetPosition: number;
|
||||
const isUpward = fromIndex > toIndex;
|
||||
const envs = await this.envs();
|
||||
if (toIndex === 0 || toIndex === envs.length - 1) {
|
||||
targetPosition = isUpward
|
||||
? envs[0].position * 2
|
||||
: envs[toIndex].position / 2;
|
||||
} else {
|
||||
targetPosition = isUpward
|
||||
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
|
||||
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
||||
}
|
||||
const newDoc = await this.update({
|
||||
_id,
|
||||
position: targetPosition,
|
||||
});
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
public async envs(
|
||||
searchText: string = '',
|
||||
sort: any = { position: -1 },
|
||||
query: any = {},
|
||||
): Promise<Env[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText);
|
||||
condition = {
|
||||
$or: [
|
||||
{
|
||||
value: reg,
|
||||
},
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
remarks: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
const newDocs = await this.find(condition, sort);
|
||||
return newDocs;
|
||||
}
|
||||
|
||||
private async find(query: any, sort: any): Promise<Env[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb
|
||||
.find(query)
|
||||
.sort({ ...sort })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async get(_id: string): Promise<Env> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb.find({ _id }).exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async getBySort(sort: any): Promise<Env> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb
|
||||
.find({})
|
||||
.sort({ ...sort })
|
||||
.limit(1)
|
||||
.exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async disabled(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.envDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: EnvStatus.disabled } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
await this.set_envs();
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async enabled(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.envDb.update(
|
||||
{ _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 }) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.envDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { name } },
|
||||
{ multi: true },
|
||||
async (err, num) => {
|
||||
await this.set_envs();
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async set_envs() {
|
||||
const envs = await this.envs(
|
||||
'',
|
||||
{ position: -1 },
|
||||
{ name: { $exists: true } },
|
||||
);
|
||||
const groups = _.groupBy(envs, 'name');
|
||||
let env_string = '';
|
||||
for (const key in groups) {
|
||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||
const group = groups[key];
|
||||
|
||||
// 忽略不符合bash要求的环境变量名称
|
||||
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
||||
env_string += `export ${key}="${_(group)
|
||||
.filter((x) => x.status !== EnvStatus.disabled)
|
||||
.map('value')
|
||||
.join('&')
|
||||
.replace(/ /g, '')}"\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(config.envFile, env_string);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import UserService from './user';
|
||||
import got from 'got';
|
||||
import nodemailer from 'nodemailer';
|
||||
import crypto from 'crypto';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
|
||||
@Service()
|
||||
export default class NotificationService {
|
||||
@Inject((type) => UserService)
|
||||
private userService!: UserService;
|
||||
|
||||
private modeMap = new Map([
|
||||
['goCqHttpBot', this.goCqHttpBot],
|
||||
['serverChan', this.serverChan],
|
||||
['bark', this.bark],
|
||||
['telegramBot', this.telegramBot],
|
||||
['dingtalkBot', this.dingtalkBot],
|
||||
['weWorkBot', this.weWorkBot],
|
||||
['weWorkApp', this.weWorkApp],
|
||||
['iGot', this.iGot],
|
||||
['pushPlus', this.pushPlus],
|
||||
['email', this.email],
|
||||
]);
|
||||
|
||||
private timeout = 10000;
|
||||
private title = '';
|
||||
private content = '';
|
||||
private params!: Omit<NotificationInfo, 'type'>;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public async notify(
|
||||
title: string,
|
||||
content: string,
|
||||
): Promise<boolean | undefined> {
|
||||
const { type, ...rest } = await this.userService.getNotificationMode();
|
||||
if (type) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.params = rest;
|
||||
const notificationModeAction = this.modeMap.get(type);
|
||||
try {
|
||||
return await notificationModeAction?.call(this);
|
||||
} catch (error: any) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async testNotify(
|
||||
info: NotificationInfo,
|
||||
title: string,
|
||||
content: string,
|
||||
) {
|
||||
const { type, ...rest } = info;
|
||||
if (type) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.params = rest;
|
||||
const notificationModeAction = this.modeMap.get(type);
|
||||
return await notificationModeAction?.call(this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async goCqHttpBot() {
|
||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||
const res: any = await got
|
||||
.post(
|
||||
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`,
|
||||
{
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: { message: `${this.title}\n${this.content}` },
|
||||
},
|
||||
)
|
||||
.json();
|
||||
return res.retcode === 0;
|
||||
}
|
||||
|
||||
private async serverChan() {
|
||||
const { serverChanKey } = this.params;
|
||||
const url = serverChanKey.startsWith('SCT')
|
||||
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
||||
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `title=${this.title}&desp=${this.content}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
return res.errno === 0 || res.data.errno === 0;
|
||||
}
|
||||
|
||||
private async bark() {
|
||||
let { barkPush, 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,
|
||||
)}?sound=${barkSound}&group=${barkGroup}`;
|
||||
const res: any = await got
|
||||
.get(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
return res.code === 200;
|
||||
}
|
||||
|
||||
private async telegramBot() {
|
||||
const {
|
||||
telegramBotApiHost,
|
||||
telegramBotProxyAuth,
|
||||
telegramBotProxyHost,
|
||||
telegramBotProxyPort,
|
||||
telegramBotToken,
|
||||
telegramBotUserId,
|
||||
} = this.params;
|
||||
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
|
||||
const url = `https://${
|
||||
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
|
||||
}/bot${telegramBotToken}/sendMessage`;
|
||||
let agent;
|
||||
if (telegramBotProxyHost && telegramBotProxyPort) {
|
||||
const options: any = {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 256,
|
||||
maxFreeSockets: 256,
|
||||
proxy: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
|
||||
};
|
||||
const httpAgent = new HttpProxyAgent(options);
|
||||
const httpsAgent = new HttpsProxyAgent(options);
|
||||
agent = {
|
||||
http: httpAgent,
|
||||
https: httpsAgent,
|
||||
};
|
||||
}
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
agent,
|
||||
})
|
||||
.json();
|
||||
return !!res.ok;
|
||||
}
|
||||
|
||||
private async dingtalkBot() {
|
||||
const { dingtalkBotSecret, dingtalkBotToken } = this.params;
|
||||
let secretParam = '';
|
||||
if (dingtalkBotSecret) {
|
||||
const dateNow = Date.now();
|
||||
const hmac = crypto.createHmac('sha256', dingtalkBotSecret);
|
||||
hmac.update(`${dateNow}\n${dingtalkBotSecret}`);
|
||||
const result = encodeURIComponent(hmac.digest('base64'));
|
||||
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
||||
}
|
||||
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: ` ${this.title}\n\n${this.content}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return res.errcode === 0;
|
||||
}
|
||||
|
||||
private async weWorkBot() {
|
||||
const { weWorkBotKey } = this.params;
|
||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: ` ${this.title}\n\n${this.content}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return res.errcode === 0;
|
||||
}
|
||||
|
||||
private async weWorkApp() {
|
||||
const { weWorkAppKey } = this.params;
|
||||
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||
weWorkAppKey.split(',');
|
||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
||||
const { access_token } = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
corpid,
|
||||
corpsecret,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
let options: any = {
|
||||
msgtype: 'mpnews',
|
||||
mpnews: {
|
||||
articles: [
|
||||
{
|
||||
title: `${this.title}`,
|
||||
thumb_media_id,
|
||||
author: `智能助手`,
|
||||
content_source_url: ``,
|
||||
content: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||
digest: `${this.content}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
switch (thumb_media_id) {
|
||||
case '0':
|
||||
options = {
|
||||
msgtype: 'textcard',
|
||||
textcard: {
|
||||
title: `${this.title}`,
|
||||
description: `${this.content}`,
|
||||
url: 'https://github.com/whyour/qinglong',
|
||||
btntxt: '更多',
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case '1':
|
||||
options = {
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: `${this.title}\n\n${this.content}`,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
const res: any = await got
|
||||
.post(
|
||||
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
|
||||
{
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
touser,
|
||||
agentid,
|
||||
safe: '0',
|
||||
...options,
|
||||
},
|
||||
},
|
||||
)
|
||||
.json();
|
||||
|
||||
return res.errcode === 0;
|
||||
}
|
||||
|
||||
private async iGot() {
|
||||
const { iGotPushKey } = this.params;
|
||||
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `title=${this.title}&content=${this.content}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
|
||||
return res.ret === 0;
|
||||
}
|
||||
|
||||
private async pushPlus() {
|
||||
const { pushPlusToken, pushPlusUser } = this.params;
|
||||
const url = `https://www.pushplus.plus/send`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: {
|
||||
token: `${pushPlusToken}`,
|
||||
title: `${this.title}`,
|
||||
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||
topic: `${pushPlusUser || ''}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
return res.code === 200;
|
||||
}
|
||||
|
||||
private async email() {
|
||||
const { emailPass, emailService, emailUser } = this.params;
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: emailService,
|
||||
auth: {
|
||||
user: emailUser,
|
||||
pass: emailPass,
|
||||
},
|
||||
});
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: `"青龙快讯" <${emailUser}>`,
|
||||
to: `${emailUser}`,
|
||||
subject: `${this.title}`,
|
||||
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||
});
|
||||
|
||||
transporter.close();
|
||||
|
||||
return !!info.messageId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { createRandomString } from '../config/util';
|
||||
import config from '../config';
|
||||
import DataStore from 'nedb';
|
||||
import { App } from '../data/open';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { dbs } from '../loaders/db';
|
||||
|
||||
@Service()
|
||||
export default class OpenService {
|
||||
private appDb = dbs.appDb;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public async findTokenByValue(token: string): Promise<App> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.find(
|
||||
{ tokens: { $elemMatch: { value: token } } },
|
||||
(err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs[0]);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async create(payload: App): Promise<App> {
|
||||
const tab = new App({ ...payload });
|
||||
tab.client_id = createRandomString(12, 12);
|
||||
tab.client_secret = createRandomString(24, 24);
|
||||
const docs = await this.insert([tab]);
|
||||
return { ...docs[0], tokens: [] };
|
||||
}
|
||||
|
||||
public async insert(payloads: App[]): Promise<App[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.insert(payloads, (err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async update(payload: App): Promise<App> {
|
||||
const { _id, client_id, client_secret, tokens, ...other } = payload;
|
||||
const doc = await this.get(_id);
|
||||
const tab = new App({ ...doc, ...other });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
return { ...newDoc, tokens: [] };
|
||||
}
|
||||
|
||||
private async updateDb(payload: App): Promise<App> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.update(
|
||||
{ _id: payload._id },
|
||||
payload,
|
||||
{ returnUpdatedDocs: true },
|
||||
(err, num, doc, up) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(doc);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async remove(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.appDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async resetSecret(_id: string): Promise<App> {
|
||||
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;
|
||||
}
|
||||
|
||||
public async list(
|
||||
searchText: string = '',
|
||||
sort: any = {},
|
||||
query: any = {},
|
||||
): Promise<App[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText);
|
||||
condition = {
|
||||
$or: [
|
||||
{
|
||||
value: reg,
|
||||
},
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
remarks: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
const newDocs = await this.find(condition, sort);
|
||||
return newDocs.map((x) => ({ ...x, tokens: [] }));
|
||||
}
|
||||
|
||||
private async find(query: any, sort: any): Promise<App[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb
|
||||
.find(query)
|
||||
.sort({ ...sort })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async get(_id: string): Promise<App> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.find({ _id }).exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async authToken({
|
||||
client_id,
|
||||
client_secret,
|
||||
}: {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
}): Promise<any> {
|
||||
const token = uuidV4();
|
||||
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.find({ client_id, client_secret }).exec((err, docs) => {
|
||||
if (docs && docs[0]) {
|
||||
this.appDb.update(
|
||||
{ client_id, client_secret },
|
||||
{ $push: { tokens: { value: token, expiration } } },
|
||||
{},
|
||||
(err, num, doc) => {
|
||||
resolve({
|
||||
code: 200,
|
||||
data: {
|
||||
token,
|
||||
token_type: 'Bearer',
|
||||
expiration,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
resolve({ code: 400, message: 'client_id或client_seret有误' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import { Crontab } from '../data/cron';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
@Service()
|
||||
export default class ScheduleService {
|
||||
private scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
async generateSchedule({ _id = '', command, name, schedule }: Crontab) {
|
||||
this.logger.info(
|
||||
'[创建定时任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
||||
_id,
|
||||
schedule,
|
||||
name,
|
||||
command,
|
||||
);
|
||||
|
||||
this.scheduleStacks.set(
|
||||
_id,
|
||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||
try {
|
||||
exec(command, async (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
await this.logger.info(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
await this.logger.info(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
stderr,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await this.logger.info(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async cancelSchedule({ _id = '', name }: Crontab) {
|
||||
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
||||
}
|
||||
}
|
||||
@@ -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,479 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { authenticator } from '@otplib/preset-default';
|
||||
import DataStore from 'nedb';
|
||||
import { AuthDataType, AuthInfo, 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 got from 'got';
|
||||
import { dbs } from '../loaders/db';
|
||||
|
||||
@Service()
|
||||
export default class UserService {
|
||||
@Inject((type) => NotificationService)
|
||||
private notificationService!: NotificationService;
|
||||
private authDb = dbs.authDb;
|
||||
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private scheduleService: ScheduleService,
|
||||
private sockService: SockService,
|
||||
) {}
|
||||
|
||||
public async login(
|
||||
payloads: {
|
||||
username: string;
|
||||
password: string;
|
||||
},
|
||||
req: Request,
|
||||
needTwoFactor = true,
|
||||
): Promise<any> {
|
||||
if (!fs.existsSync(config.authConfigFile)) {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
|
||||
let { username, password } = payloads;
|
||||
const content = this.getAuthInfo();
|
||||
const timestamp = Date.now();
|
||||
if (content) {
|
||||
let {
|
||||
username: cUsername,
|
||||
password: cPassword,
|
||||
retries = 0,
|
||||
lastlogon,
|
||||
lastip,
|
||||
lastaddr,
|
||||
twoFactorActivated,
|
||||
twoFactorActived,
|
||||
tokens = {},
|
||||
platform,
|
||||
} = content;
|
||||
// patch old field
|
||||
twoFactorActivated = twoFactorActivated || twoFactorActived;
|
||||
|
||||
if (
|
||||
(cUsername === 'admin' && cPassword === 'admin') ||
|
||||
!cUsername ||
|
||||
!cPassword
|
||||
) {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
|
||||
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
|
||||
return {
|
||||
code: 410,
|
||||
message: `失败次数过多,请${Math.round(
|
||||
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||
)}秒后重试`,
|
||||
data: Math.round(
|
||||
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const { ip, address } = await getNetIp(req);
|
||||
if (username === cUsername && password === cPassword) {
|
||||
if (twoFactorActivated && needTwoFactor) {
|
||||
this.updateAuthInfo(content, {
|
||||
isTwoFactorChecking: true,
|
||||
});
|
||||
return {
|
||||
code: 420,
|
||||
message: '请输入两步验证token',
|
||||
};
|
||||
}
|
||||
|
||||
const data = createRandomString(50, 100);
|
||||
const expiration = twoFactorActivated ? 30 : 3;
|
||||
let token = jwt.sign({ data }, config.secret as any, {
|
||||
expiresIn: 60 * 60 * 24 * expiration,
|
||||
algorithm: 'HS384',
|
||||
});
|
||||
|
||||
this.updateAuthInfo(content, {
|
||||
token,
|
||||
tokens: {
|
||||
...tokens,
|
||||
[req.platform]: token,
|
||||
},
|
||||
lastlogon: timestamp,
|
||||
retries: 0,
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
isTwoFactorChecking: false,
|
||||
});
|
||||
await this.notificationService.notify(
|
||||
'登录通知',
|
||||
`你于${new Date(timestamp).toLocaleString()}在 ${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(
|
||||
'登录通知',
|
||||
`你于${new Date(timestamp).toLocaleString()}在 ${address} ${
|
||||
req.platform
|
||||
}端 登录失败,ip地址 ${ip}`,
|
||||
);
|
||||
await this.getLoginLog();
|
||||
await this.insertDb({
|
||||
type: AuthDataType.loginLog,
|
||||
info: {
|
||||
timestamp,
|
||||
address,
|
||||
ip,
|
||||
platform: req.platform,
|
||||
status: LoginStatus.fail,
|
||||
},
|
||||
});
|
||||
return { code: 400, message: config.authError };
|
||||
}
|
||||
} else {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
}
|
||||
|
||||
public async logout(platform: string): Promise<any> {
|
||||
const authInfo = this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, {
|
||||
token: '',
|
||||
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||
});
|
||||
}
|
||||
|
||||
public async getLoginLog(): Promise<AuthInfo[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb.find({ type: AuthDataType.loginLog }).exec((err, docs) => {
|
||||
if (err || docs.length === 0) {
|
||||
resolve([]);
|
||||
} else {
|
||||
const result = docs.sort(
|
||||
(a, b) => b.info.timestamp - a.info.timestamp,
|
||||
);
|
||||
if (result.length > 100) {
|
||||
this.authDb.remove({ _id: result[result.length - 1]._id });
|
||||
}
|
||||
resolve(result.map((x) => x.info));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb.insert(payload, (err, doc) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(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 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> {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb
|
||||
.find({ type: AuthDataType.notification })
|
||||
.exec((err, docs) => {
|
||||
if (err || docs.length === 0) {
|
||||
resolve({} as NotificationInfo);
|
||||
} else {
|
||||
resolve(docs[0].info);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async getLogRemoveFrequency() {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb
|
||||
.find({ type: AuthDataType.removeLogFrequency })
|
||||
.exec((err, docs) => {
|
||||
if (err || docs.length === 0) {
|
||||
resolve({});
|
||||
} else {
|
||||
resolve(docs[0].info);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb.update(
|
||||
{ type: payload.type },
|
||||
{ ...payload },
|
||||
{ upsert: true, returnUpdatedDocs: true },
|
||||
(err, num, doc: any) => {
|
||||
if (err) {
|
||||
resolve({} as NotificationInfo);
|
||||
} else {
|
||||
resolve({ ...doc.info, _id: doc._id });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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, data: '通知发送失败,请检查参数' };
|
||||
}
|
||||
}
|
||||
|
||||
public async updateLogRemoveFrequency(frequency: number) {
|
||||
const result = await this.updateAuthDb({
|
||||
type: AuthDataType.removeLogFrequency,
|
||||
info: { frequency },
|
||||
});
|
||||
const cron = {
|
||||
_id: result._id,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${frequency}`,
|
||||
schedule: `5 23 */${frequency} * *`,
|
||||
};
|
||||
await this.scheduleService.cancelSchedule(cron);
|
||||
if (frequency > 0) {
|
||||
await this.scheduleService.generateSchedule(cron);
|
||||
}
|
||||
return { code: 200, data: { ...cron } };
|
||||
}
|
||||
|
||||
public async checkUpdate() {
|
||||
try {
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
const lastVersionFileContent = await (
|
||||
await got.get(config.lastVersionFile)
|
||||
).body;
|
||||
const lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||
const lastLog = lastVersionFileContent.match(logRegx)
|
||||
? lastVersionFileContent.match(logRegx)![1]
|
||||
: '';
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
||||
lastVersion,
|
||||
lastLog,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
code: 400,
|
||||
data: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private checkHasNewVersion(curVersion: string, lastVersion: string) {
|
||||
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
|
||||
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
|
||||
if (curArr[0] < lastArr[0]) {
|
||||
return true;
|
||||
}
|
||||
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
curArr[0] === lastArr[0] &&
|
||||
curArr[1] === lastArr[1] &&
|
||||
curArr[2] < lastArr[2]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async updateSystem() {
|
||||
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: `开始更新系统`,
|
||||
});
|
||||
cp.stdout.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.on('error', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: JSON.stringify(err),
|
||||
});
|
||||
});
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
version: '2'
|
||||
services:
|
||||
web:
|
||||
image: whyour/qinglong:latest
|
||||
volumes:
|
||||
- ./data/config:/ql/config
|
||||
- ./data/log:/ql/log
|
||||
- ./data/db:/ql/db
|
||||
- ./data/scripts:/ql/scripts
|
||||
- ./data/repo:/ql/repo
|
||||
ports:
|
||||
- "0.0.0.0:5700:5700"
|
||||
environment:
|
||||
- ENABLE_HANGUP=true
|
||||
- ENABLE_WEB_PANEL=true
|
||||
restart: always
|
||||
+25
-22
@@ -1,14 +1,19 @@
|
||||
FROM node:lts-alpine
|
||||
LABEL maintainer="whyour"
|
||||
ARG QL_BASE_URL=https://github.com.cnpmjs.org/whyour/qinglong
|
||||
ARG QL_BASE_BRANCH=master
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||
ARG QL_BRANCH=master
|
||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
LANG=zh_CN.UTF-8 \
|
||||
SHELL=/bin/bash \
|
||||
PS1="\u@\h:\w \$ " \
|
||||
QL_DIR=/ql
|
||||
QL_DIR=/ql \
|
||||
QL_MAINTAINER=${QL_MAINTAINER} \
|
||||
QL_URL=${QL_URL} \
|
||||
QL_BRANCH=${QL_BRANCH}
|
||||
WORKDIR ${QL_DIR}
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
RUN set -x \
|
||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
|
||||
&& apk update -f \
|
||||
&& apk upgrade \
|
||||
&& apk --no-cache add -f bash \
|
||||
@@ -16,35 +21,33 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
moreutils \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
tzdata \
|
||||
perl \
|
||||
openssl \
|
||||
nginx \
|
||||
python3 \
|
||||
jq \
|
||||
openssh \
|
||||
py3-pip \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& touch ~/.bashrc \
|
||||
&& mkdir /run/nginx \
|
||||
&& git clone -b ${QL_BASE_BRANCH} ${QL_BASE_URL} ${QL_DIR} \
|
||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||
&& git config --global user.name "qinglong" \
|
||||
&& cd ${QL_DIR} \
|
||||
&& cp -f .env.example .env \
|
||||
&& ln -sf ${QL_DIR}/shell/js.sh /usr/local/bin/js \
|
||||
&& ln -sf ${QL_DIR}/shell/git_pull.sh /usr/local/bin/git_pull \
|
||||
&& ln -sf ${QL_DIR}/shell/rm_log.sh /usr/local/bin/rm_log \
|
||||
&& ln -sf ${QL_DIR}/shell/export_sharecodes.sh /usr/local/bin/export_sharecodes \
|
||||
&& ln -sf ${QL_DIR}/shell/git_diy.sh /usr/local/bin/diy \
|
||||
&& ln -sf ${QL_DIR}/shell/notify.sh /usr/local/bin/notify \
|
||||
&& ln -sf ${QL_DIR}/shell/rebuild.sh /usr/local/bin/rebuild \
|
||||
&& ln -sf ${QL_DIR}/docker/docker-entrypoint.sh /usr/local/bin/entrypoint \
|
||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||
&& npm install -g pm2 \
|
||||
&& npm install -g pnpm \
|
||||
&& pnpm install -g pm2 \
|
||||
&& pnpm install -g ts-node typescript tslib \
|
||||
&& rm -rf /root/.npm \
|
||||
&& yarn install --network-timeout 100000 \
|
||||
&& yarn build \
|
||||
&& yarn build-back \
|
||||
&& yarn install --production --network-timeout 100000 \
|
||||
&& yarn cache clean
|
||||
ENTRYPOINT ["entrypoint"]
|
||||
&& pnpm install --prod \
|
||||
&& rm -rf /root/.pnpm-store \
|
||||
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||
&& cp -rf /static/* ${QL_DIR} \
|
||||
&& rm -rf /static
|
||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||
|
||||
+40
-53
@@ -1,64 +1,51 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
dir_shell=/ql/shell
|
||||
. $dir_shell/share.sh
|
||||
link_shell
|
||||
|
||||
echo -e "======================1. 检测配置文件========================\n"
|
||||
[ ! -d ${QL_DIR}/config ] && mkdir -p ${QL_DIR}/config
|
||||
[ ! -d ${QL_DIR}/log ] && mkdir -p ${QL_DIR}/log
|
||||
[ ! -d ${QL_DIR}/db ] && mkdir -p ${QL_DIR}/db
|
||||
[ ! -d ${QL_DIR}/manual_log ] && mkdir -p ${QL_DIR}/manual_log
|
||||
|
||||
if [ ! -s ${QL_DIR}/config/crontab.list ]
|
||||
then
|
||||
echo -e "检测到config配置目录下不存在crontab.list或存在但文件为空,从示例文件复制一份用于初始化...\n"
|
||||
cp -fv ${QL_DIR}/sample/crontab.list.sample ${QL_DIR}/config/crontab.list
|
||||
sed -i "s,MY_PATH,${QL_DIR},g" ${QL_DIR}/config/crontab.list
|
||||
sed -i "s,ENV_PATH=,PATH=$PATH,g" ${QL_DIR}/config/crontab.list
|
||||
fi
|
||||
crond
|
||||
crontab ${QL_DIR}/config/crontab.list
|
||||
echo -e "成功添加定时任务...\n"
|
||||
|
||||
if [ ! -s ${QL_DIR}/config/cookie.sh ]; then
|
||||
echo -e "检测到config配置目录下不存在cookie.sh,从示例文件复制一份用于初始化...\n"
|
||||
touch ${QL_DIR}/config/cookie.sh
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ ! -s ${QL_DIR}/config/config.sh ]; then
|
||||
echo -e "检测到config配置目录下不存在config.sh,从示例文件复制一份用于初始化...\n"
|
||||
cp -fv ${QL_DIR}/sample/config.sh.sample ${QL_DIR}/config/config.sh
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ ! -s ${QL_DIR}/config/auth.json ]; then
|
||||
echo -e "检测到config配置目录下不存在auth.json,从示例文件复制一份用于初始化...\n"
|
||||
cp -fv ${QL_DIR}/sample/auth.json ${QL_DIR}/config/auth.json
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ -s /etc/nginx/conf.d/default.conf ]; then
|
||||
echo -e "检测到默认nginx配置文件,删除...\n"
|
||||
rm -f /etc/nginx/conf.d/default.conf
|
||||
echo
|
||||
fi
|
||||
|
||||
cp -fv ${QL_DIR}/docker/front.conf /etc/nginx/conf.d/front.conf
|
||||
|
||||
echo -e "======================2. 启动nginx========================\n"
|
||||
nginx -c /etc/nginx/nginx.conf
|
||||
make_dir /etc/nginx/conf.d
|
||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||
pm2 l &>/dev/null
|
||||
echo
|
||||
|
||||
echo -e "======================3. 启动控制面板========================\n"
|
||||
pm2 start ${QL_DIR}/build/app.js -n panel
|
||||
echo -e "======================2. 安装依赖========================\n"
|
||||
update_depend
|
||||
echo
|
||||
|
||||
echo -e "======================3. 启动nginx========================\n"
|
||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||
echo -e "nginx启动成功...\n"
|
||||
|
||||
echo -e "======================4. 启动控制面板========================\n"
|
||||
pm2 delete panel &>/dev/null
|
||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time
|
||||
echo -e "控制面板启动成功...\n"
|
||||
|
||||
echo -e "\n容器启动成功...\n"
|
||||
echo -e "\n请先访问5700端口,登录面板成功之后先手动执行一次git_pull命令...\n"
|
||||
echo -e "\n如果需要启动挂机程序手动执行docker exec -it qinglong js hangup...\n"
|
||||
echo -e "\n或者去cron管理搜索hangup手动执行挂机任务...\n"
|
||||
echo -e "======================5. 启动定时任务========================\n"
|
||||
pm2 delete schedule &>/dev/null
|
||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
|
||||
echo -e "定时任务启动成功...\n"
|
||||
|
||||
if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then
|
||||
set -- node "$@"
|
||||
if [[ $AutoStartBot == true ]]; then
|
||||
echo -e "======================6. 启动bot========================\n"
|
||||
nohup ql bot >>$dir_log/start.log 2>&1 &
|
||||
echo -e "bot后台启动中...\n"
|
||||
fi
|
||||
|
||||
if [[ $EnableExtraShell == true ]]; then
|
||||
echo -e "======================7. 执行自定义脚本========================\n"
|
||||
nohup ql extra >>$dir_log/start.log 2>&1 &
|
||||
echo -e "自定义脚本后台执行中...\n"
|
||||
fi
|
||||
|
||||
echo -e "############################################################\n"
|
||||
echo -e "容器启动成功..."
|
||||
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
|
||||
echo -e "############################################################\n"
|
||||
|
||||
crond -f >/dev/null
|
||||
|
||||
exec "$@"
|
||||
+19
-1
@@ -1,5 +1,10 @@
|
||||
upstream api {
|
||||
server localhost:5600;
|
||||
server 0.0.0.0:5600;
|
||||
}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default keep-alive;
|
||||
'websocket' upgrade;
|
||||
}
|
||||
|
||||
server {
|
||||
@@ -8,6 +13,19 @@ server {
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
location /api {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://api;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
location /open {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://api;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+41
-13
@@ -5,7 +5,8 @@
|
||||
"build": "umi build",
|
||||
"build-back": "tsc -p tsconfig.back.json",
|
||||
"start-back": "nodemon",
|
||||
"pm2": "npm run build-back && node build/app.js",
|
||||
"panel": "npm run build-back && node build/app.js",
|
||||
"schedule": "npm run build-back && node build/schedule.js",
|
||||
"prepare": "umi generate tmp",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"test": "umi-test",
|
||||
@@ -23,52 +24,79 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@otplib/preset-default": "^12.0.1",
|
||||
"@sentry/node": "^6.14.0",
|
||||
"@sentry/tracing": "^6.14.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"celebrate": "^13.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^3.3.0",
|
||||
"cron-parser": "^3.5.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-jwt": "^6.0.0",
|
||||
"express-urlrewrite": "^1.4.0",
|
||||
"got": "^11.8.2",
|
||||
"hpagent": "^0.1.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nedb": "^1.8.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-schedule": "^2.0.0",
|
||||
"nodemailer": "^6.7.0",
|
||||
"p-queue": "6.6.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"serve-handler": "^6.1.3",
|
||||
"sockjs": "^0.3.21",
|
||||
"typedi": "^0.8.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"umi": "^3.3.9",
|
||||
"umi-request": "^1.3.5",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-diff-viewer": "^3.1.1",
|
||||
"react-dnd": "^14.0.2",
|
||||
"react-dnd-html5-backend": "^14.0.0",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"darkreader": "^4.9.27",
|
||||
"codemirror": "^5.59.4",
|
||||
"@ant-design/pro-layout": "^6.5.0",
|
||||
"@umijs/plugin-antd": "^0.9.1",
|
||||
"@ant-design/icons": "^4.6.2",
|
||||
"@ant-design/pro-layout": "^6.26.0",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@sentry/react": "^6.14.0",
|
||||
"@sentry/webpack-plugin": "^1.18.3",
|
||||
"@types/cors": "^2.8.10",
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/express-jwt": "^6.0.1",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/lodash": "^4.14.170",
|
||||
"@types/nedb": "^1.8.11",
|
||||
"@types/node": "^14.11.2",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/qrcode.react": "^1.0.1",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/serve-handler": "^6.1.1",
|
||||
"@types/sockjs": "^0.3.33",
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
"@umijs/plugin-antd": "^0.11.0",
|
||||
"@umijs/test": "^3.3.9",
|
||||
"antd": "^4.17.0-alpha.6",
|
||||
"codemirror": "^5.62.2",
|
||||
"compression-webpack-plugin": "6.1.1",
|
||||
"darkreader": "^4.9.27",
|
||||
"lint-staged": "^10.0.7",
|
||||
"nodemon": "^2.0.4",
|
||||
"prettier": "^2.2.0",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "17.x",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-diff-viewer": "^3.1.1",
|
||||
"react-dnd": "^14.0.2",
|
||||
"react-dnd-html5-backend": "^14.0.0",
|
||||
"react-dom": "17.x",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.5.2",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.1.2",
|
||||
"umi": "^3.5.0",
|
||||
"umi-request": "^1.3.5",
|
||||
"vh-check": "^2.0.5",
|
||||
"webpack": "^5.28.0",
|
||||
"yorkie": "^2.0.0"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
@@ -1 +0,0 @@
|
||||
{ "username": "admin", "password": "adminadmin" }
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
{ "username": "admin", "password": "admin" }
|
||||
@@ -0,0 +1,115 @@
|
||||
## Version: v2.8.0
|
||||
## Date: 2021-06-20
|
||||
## Update Content: 可持续发展纲要\n1. session管理破坏性修改\n2. 配置管理可编辑config下文件\n3. 自定义脚本改为查看脚本\n4. 移除互助相关
|
||||
|
||||
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
|
||||
|
||||
## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务
|
||||
AutoDelCron="true"
|
||||
|
||||
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
|
||||
AutoAddCron="true"
|
||||
|
||||
## 拉取脚本时默认的定时规则,当匹配不到定时规则时使用,例如: 0 9 * * *
|
||||
DefaultCronRule=""
|
||||
|
||||
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
|
||||
RepoFileExtensions="js py"
|
||||
|
||||
## 由于github仓库拉取较慢,所以会默认添加代理前缀,如不需要请移除
|
||||
GithubProxyUrl="https://ghproxy.com/"
|
||||
|
||||
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||
CommandTimeoutTime="1h"
|
||||
|
||||
## 设置批量执行任务时的并发数,默认同时执行5个任务
|
||||
MaxConcurrentNum="5"
|
||||
|
||||
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
|
||||
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
|
||||
RandomDelay="300"
|
||||
|
||||
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
|
||||
EnableExtraShell="true"
|
||||
|
||||
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
|
||||
AutoStartBot=""
|
||||
|
||||
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
||||
PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
|
||||
## 通知环境变量
|
||||
## 1. Server酱
|
||||
## https://sct.ftqq.com
|
||||
## 下方填写 SCHKEY 值或 SendKey 值
|
||||
export PUSH_KEY=""
|
||||
|
||||
## 2. BARK
|
||||
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
|
||||
export BARK_PUSH=""
|
||||
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
|
||||
export BARK_SOUND=""
|
||||
## 下方填写推送消息分组,默认为"QingLong"
|
||||
export BARK_GROUP="QingLong"
|
||||
|
||||
## 3. Telegram
|
||||
## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw
|
||||
export TG_BOT_TOKEN=""
|
||||
## 下方填写 @getuseridbot 中获取到的纯数字ID
|
||||
export TG_USER_ID=""
|
||||
## Telegram 代理IP(选填)
|
||||
## 下方填写代理IP地址,代理类型为 http,比如您代理是 http://127.0.0.1:1080,则填写 "127.0.0.1"
|
||||
## 如需使用,请自行解除下一行的注释
|
||||
export TG_PROXY_HOST=""
|
||||
## Telegram 代理端口(选填)
|
||||
## 下方填写代理端口号,代理类型为 http,比如您代理是 http://127.0.0.1:1080,则填写 "1080"
|
||||
## 如需使用,请自行解除下一行的注释
|
||||
export TG_PROXY_PORT=""
|
||||
## Telegram 代理的认证参数(选填)
|
||||
export TG_PROXY_AUTH=""
|
||||
## Telegram api自建反向代理地址(选填)
|
||||
## 教程:https://www.hostloc.com/thread-805441-1-1.html
|
||||
## 如反向代理地址 http://aaa.bbb.ccc 则填写 aaa.bbb.ccc
|
||||
## 如需使用,请赋值代理地址链接,并自行解除下一行的注释
|
||||
export TG_API_HOST=""
|
||||
|
||||
## 4. 钉钉
|
||||
## 官方文档:https://developers.dingtalk.com/document/app/custom-robot-access
|
||||
## 下方填写token后面的内容,只需 https://oapi.dingtalk.com/robot/send?access_token=XXX 等于=符号后面的XXX即可
|
||||
export DD_BOT_TOKEN=""
|
||||
export DD_BOT_SECRET=""
|
||||
|
||||
## 5. 企业微信机器人
|
||||
## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
|
||||
## 下方填写密钥,企业微信推送 webhook 后面的 key
|
||||
export QYWX_KEY=""
|
||||
|
||||
## 6. 企业微信应用
|
||||
## 参考文档:http://note.youdao.com/s/HMiudGkb
|
||||
## 下方填写素材库图片id(corpid,corpsecret,touser,agentid),素材库图片填0为图文消息, 填1为纯文本消息
|
||||
export QYWX_AM=""
|
||||
|
||||
## 7. iGot聚合
|
||||
## 参考文档:https://wahao.github.io/Bark-MP-helper
|
||||
## 下方填写iGot的推送key,支持多方式推送,确保消息可达
|
||||
export IGOT_PUSH_KEY=""
|
||||
|
||||
## 8. Push Plus
|
||||
## 官方网站:http://www.pushplus.plus
|
||||
## 下方填写您的Token,微信扫码登录后一对一推送或一对多推送下面的token,只填 PUSH_PLUS_TOKEN 默认为一对一推送
|
||||
export PUSH_PLUS_TOKEN=""
|
||||
## 一对一多推送(选填)
|
||||
## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码)
|
||||
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
|
||||
export PUSH_PLUS_USER=""
|
||||
|
||||
## 9. go-cqhttp
|
||||
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||
## gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||
## 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
|
||||
export GOBOT_URL=""
|
||||
export GOBOT_TOKEN=""
|
||||
export GOBOT_QQ=""
|
||||
|
||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||
@@ -1,599 +0,0 @@
|
||||
## Version: v3.30.6
|
||||
## Date: 2021-3-8
|
||||
## Update Content: 京小兑控制变量,宠汪汪自己内部账户互助变量,环球挑战赛互助变量。
|
||||
|
||||
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
|
||||
|
||||
################################## 说明 ##################################
|
||||
## 以下配置中,带有 export 申明的,均由LXK9301大佬定义,详见:https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
|
||||
## 其他互助码,考虑到直接按LXK9301大佬定义的变量去填的话,一是不方便记忆,二是容易搞混,所以最终的变量将由脚本去组合,你只要按注释去填即可
|
||||
## 除此之外,还额外增加了是否自动删除失效任务AutoDelCron、是否自动增加新任务AutoAddCron、删除旧日志时间RmLogDaysAgo、随机延迟启动任务RandomDelay、是否添加自定义脚本EnableExtraShell五个人性化的设置供选择
|
||||
## 所有赋值等号两边不能有空格,所有的值请一律在两侧添加半角的双引号,如果变量值中含有双引号,则外侧改为一对半角的单引号。
|
||||
## 所有的赋值都可以参考 “定义东东萌宠是否静默运行” 部分,在不同时间设置不同的值,以达到你想要的效果,具体判断条件如下:
|
||||
## $(date "+%d") 当前的日期,如:13
|
||||
## $(date "+%w") 当前是星期几,如:3
|
||||
## $(date "+%H") 当前的小时数,如:23
|
||||
## $(date "+%M") 当前的分钟数,如:49
|
||||
## 其他date命令的更多用法,可以在命令行中输入 date --help 查看
|
||||
## 判断条件 -eq -ne -gt -ge -lt -le ,具体含义可百度一下
|
||||
|
||||
|
||||
################################## 临时屏蔽某个Cookie(选填) ##################################
|
||||
## 如果某些Cookie已经失效了,但暂时还没法更新,可以使用此功能在不删除该Cookie和重新修改Cookie编号的前提下,临时屏蔽掉某些编号的Cookie
|
||||
## 多个Cookie编号以半角的空格分隔,两侧一对半角双引号,使用此功能后,在运行js脚本时账户编号将发生变化
|
||||
## 举例1:TempBlockCookie="2" 临时屏蔽掉Cookie2
|
||||
## 举例2:TempBlockCookie="2 4" 临时屏蔽掉Cookie2和Cookie4
|
||||
## 如果只是想要屏蔽某个账号不玩某些小游戏,可以参考下面 case 这个命令的例子来控制,脚本名称请去掉后缀 “.js”
|
||||
## case $1 in
|
||||
## jd_fruit)
|
||||
## TempBlockCookie="5" # 账号5不玩东东农场
|
||||
## ;;
|
||||
## jd_dreamFactory | jd_jdfactory)
|
||||
## TempBlockCookie="2" # 账号2不玩京喜工厂和东东工厂
|
||||
## ;;
|
||||
## jd_jdzz | jd_joy)
|
||||
## TempBlockCookie="3 6" # 账号3、账号6不玩京东赚赚和宠汪汪
|
||||
## ;;
|
||||
## esac
|
||||
TempBlockCookie=""
|
||||
|
||||
|
||||
################################## 定义是否自动删除失效的脚本与定时任务(选填) ##################################
|
||||
## 有的时候,某些JS脚本只在特定的时间有效,过了时间就失效了,需要自动删除失效的本地定时任务,则设置为 "true" ,否则请设置为 "false"
|
||||
## 检测文件:LXK9301/jd_scripts 仓库中的 docker/crontab_list.sh,和 shylocks/Loon 仓库中的 docker/crontab_list.sh
|
||||
## 当设置为 "true" 时,会自动从检测文件中读取比对删除的任务(识别以“jd_”、“jr_”、“jx_”开头的任务)
|
||||
## 当设置为 "true" 时,脚本只会删除一整行失效的定时任务,不会修改其他现有任务,所以任何时候,你都可以自己调整你的crontab.list
|
||||
## 当设置为 "true" 时,如果你有添加额外脚本是以“jd_”“jr_”“jx_”开头的,如检测文件中,会被删除,不是以“jd_”“jr_”“jx_”开头的任务则不受影响
|
||||
AutoDelCron="true"
|
||||
|
||||
|
||||
################################## 定义是否自动增加新的本地定时任务(选填) ##################################
|
||||
## LXK9301 大佬会在有需要的时候,增加定时任务,如需要本地自动增加新的定时任务,则设置为 "true" ,否则请设置为 "false"
|
||||
## 检测文件:LXK9301/jd_scripts 仓库中的 docker/crontab_list.sh,和 shylocks/Loon 仓库中的 docker/crontab_list.sh
|
||||
## 当设置为 "true" 时,如果检测到检测文件中有增加新的定时任务,那么在本地也增加(识别以“jd_”、“jr_”、“jx_”开头的任务)
|
||||
## 当设置为 "true" 时,会自动从检测文件新增加的任务中读取时间,该时间为北京时间
|
||||
## 当设置为 "true" 时,脚本只会增加新的定时任务,不会修改其他现有任务,所以任何时候,你都可以自己调整你的crontab.list
|
||||
AutoAddCron="true"
|
||||
|
||||
|
||||
################################## 定义删除日志的时间(选填) ##################################
|
||||
## 定义在运行删除旧的日志任务时,要删除多少天以前的日志,请输入正整数,不填则禁用删除日志的功能
|
||||
RmLogDaysAgo="7"
|
||||
|
||||
|
||||
################################## 定义随机延迟启动任务(选填) ##################################
|
||||
## 如果任务不是必须准点运行的任务,那么给它增加一个随机延迟,由你定义最大延迟时间,单位为秒,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行
|
||||
## 在crontab.list中,在每小时第0-2分、第30-31分、第59分这几个时间内启动的任务,均算作必须准点运行的任务,在启动这些任务时,即使你定义了RandomDelay,也将准点运行,不启用随机延迟
|
||||
## 在crontab.list中,除掉每小时上述时间启动的任务外,其他任务在你定义了 RandomDelay 的情况下,一律启用随机延迟,但如果你按照Wiki教程给某些任务添加了 "now",那么这些任务也将无视随机延迟直接启动
|
||||
RandomDelay="300"
|
||||
|
||||
|
||||
################################## 定义User-Agent(选填) ##################################
|
||||
## 自定义LXK9301大佬仓库里京东系列js脚本的User-Agent,不懂不知不会User-Agent的请不要随意填写内容,随意填写了出错概不负责
|
||||
## 如需使用,请自行解除下一行注释
|
||||
# export JD_USER_AGENT=""
|
||||
|
||||
|
||||
################################## 定义通知TOKEN(选填) ##################################
|
||||
## 想通过什么渠道收取通知,就填入对应渠道的值
|
||||
## 1. ServerChan,教程:http://sc.ftqq.com/3.version
|
||||
export PUSH_KEY=""
|
||||
|
||||
## 2. BARK,教程(看BARK_PUSH和BARK_SOUND的说明):https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
|
||||
export BARK_PUSH=""
|
||||
export BARK_SOUND=""
|
||||
|
||||
## 3. Telegram,如需使用,TG_BOT_TOKEN和TG_USER_ID必须同时赋值,教程:https://gitee.com/lxk0301/jd_scripts/blob/master/backUp/TG_PUSH.md
|
||||
export TG_BOT_TOKEN=""
|
||||
export TG_USER_ID=""
|
||||
|
||||
## 4. 钉钉,教程(看DD_BOT_TOKEN和DD_BOT_SECRET部分):https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
|
||||
export DD_BOT_TOKEN=""
|
||||
export DD_BOT_SECRET=""
|
||||
|
||||
## 5. iGot聚合推送,支持多方式推送,填写iGot的推送key。教程:https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
|
||||
export IGOT_PUSH_KEY=""
|
||||
|
||||
## 6. Push Plus,微信扫码登录后一对一推送或一对多推送,参考文档:http://pushplus.hxtrip.com/
|
||||
## 其中PUSH_PLUS_USER是一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码)注:(1、需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送),只填PUSH_PLUS_TOKEN默认为一对一推送
|
||||
export PUSH_PLUS_TOKEN=""
|
||||
export PUSH_PLUS_USER=""
|
||||
|
||||
## 7. 企业微信机器人消息推送 webhook 后面的 key,文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
|
||||
export QYWX_KEY=""
|
||||
|
||||
## 8. 企业微信应用消息推送的值,教程:https://note.youdao.com/ynoteshare1/index.html?id=351e08a72378206f9dd64d2281e9b83b&type=note
|
||||
## 依次填上corpid的值,corpsecret的值,touser的值,agentid,media_id的值,注意用,号隔开,例如:"wwcff56746d9adwers,B-791548lnzXBE6_BWfxdf3kSTMJr9vFEPKAbh6WERQ,mingcheng,1000001,2COXgjH2UIfERF2zxrtUOKgQ9XklUqMdGSWLBoW_lSDAdafat"
|
||||
export QYWX_AM=""
|
||||
|
||||
|
||||
################################## 定义每日签到的通知形式(选填) ##################################
|
||||
## js脚本每日签到提供3种通知方式,分别为:
|
||||
## 关闭通知,那么请在下方填入0
|
||||
## 简洁通知,那么请在下方填入1,其效果见:https://github.com/LXK9301/jd_scripts/blob/master/icon/bean_sign_simple.jpg
|
||||
## 原始通知,那么请在下方填入2,如果不填也默认为2,内容比较长,这也是默认通知方式
|
||||
NotifyBeanSign=""
|
||||
|
||||
|
||||
################################## 定义每日签到每个接口间的延迟时间(选填) ##################################
|
||||
## 默认每个签到接口并发无延迟,如需要依次进行每个接口,请自定义延迟时间,单位为毫秒,延迟作用于每个签到接口, 如填入延迟则切换顺序签到(耗时较长)
|
||||
export JD_BEAN_STOP=""
|
||||
|
||||
|
||||
################################## 定义东东农场互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyFruit1=""
|
||||
MyFruit2=""
|
||||
MyFruit3=""
|
||||
MyFruit4=""
|
||||
MyFruit5=""
|
||||
MyFruit6=""
|
||||
MyFruitA=""
|
||||
MyFruitB=""
|
||||
|
||||
ForOtherFruit1=""
|
||||
ForOtherFruit2=""
|
||||
ForOtherFruit3=""
|
||||
ForOtherFruit4=""
|
||||
ForOtherFruit5=""
|
||||
ForOtherFruit6=""
|
||||
|
||||
|
||||
################################## 定义东东萌宠互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyPet1=""
|
||||
MyPet2=""
|
||||
MyPet3=""
|
||||
MyPet4=""
|
||||
MyPet5=""
|
||||
MyPet6=""
|
||||
MyPetA=""
|
||||
MyPetB=""
|
||||
|
||||
ForOtherPet1=""
|
||||
ForOtherPet2=""
|
||||
ForOtherPet3=""
|
||||
ForOtherPet4=""
|
||||
ForOtherPet5=""
|
||||
ForOtherPet6=""
|
||||
|
||||
|
||||
################################## 定义种豆得豆互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyBean1=""
|
||||
MyBean2=""
|
||||
MyBean3=""
|
||||
MyBean4=""
|
||||
MyBean5=""
|
||||
MyBean6=""
|
||||
MyBeanA=""
|
||||
MyBeanB=""
|
||||
|
||||
ForOtherBean1=""
|
||||
ForOtherBean2=""
|
||||
ForOtherBean3=""
|
||||
ForOtherBean4=""
|
||||
ForOtherBean5=""
|
||||
ForOtherBean6=""
|
||||
|
||||
|
||||
################################## 定义京喜工厂互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyDreamFactory1=""
|
||||
MyDreamFactory2=""
|
||||
MyDreamFactory3=""
|
||||
MyDreamFactory4=""
|
||||
MyDreamFactory5=""
|
||||
MyDreamFactory6=""
|
||||
MyDreamFactoryA=""
|
||||
MyDreamFactoryB=""
|
||||
|
||||
ForOtherDreamFactory1=""
|
||||
ForOtherDreamFactory2=""
|
||||
ForOtherDreamFactory3=""
|
||||
ForOtherDreamFactory4=""
|
||||
ForOtherDreamFactory5=""
|
||||
ForOtherDreamFactory6=""
|
||||
|
||||
|
||||
################################## 定义东东工厂互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyJdFactory1=""
|
||||
MyJdFactory2=""
|
||||
MyJdFactory3=""
|
||||
MyJdFactory4=""
|
||||
MyJdFactory5=""
|
||||
MyJdFactory6=""
|
||||
MyJdFactoryA=""
|
||||
MyJdFactoryB=""
|
||||
|
||||
ForOtherJdFactory1=""
|
||||
ForOtherJdFactory2=""
|
||||
ForOtherJdFactory3=""
|
||||
ForOtherJdFactory4=""
|
||||
ForOtherJdFactory5=""
|
||||
ForOtherJdFactory6=""
|
||||
|
||||
|
||||
################################## 定义京东赚赚互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyJdzz1=""
|
||||
MyJdzz2=""
|
||||
MyJdzz3=""
|
||||
MyJdzz4=""
|
||||
MyJdzz5=""
|
||||
MyJdzz6=""
|
||||
MyJdzzA=""
|
||||
MyJdzzB=""
|
||||
|
||||
ForOtherJdzz1=""
|
||||
ForOtherJdzz2=""
|
||||
ForOtherJdzz3=""
|
||||
ForOtherJdzz4=""
|
||||
ForOtherJdzz5=""
|
||||
ForOtherJdzz6=""
|
||||
|
||||
|
||||
################################## 定义疯狂的JOY互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyJoy1=""
|
||||
MyJoy2=""
|
||||
MyJoy3=""
|
||||
MyJoy4=""
|
||||
MyJoy5=""
|
||||
MyJoy6=""
|
||||
MyJoyA=""
|
||||
MyJoyB=""
|
||||
|
||||
ForOtherJoy1=""
|
||||
ForOtherJoy2=""
|
||||
ForOtherJoy3=""
|
||||
ForOtherJoy4=""
|
||||
ForOtherJoy5=""
|
||||
ForOtherJoy6=""
|
||||
|
||||
|
||||
################################## 定义京喜农场互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
## 京喜农场助力码为 JSON 格式因此使用单引号,json 格式如下
|
||||
## {"smp":"22bdadsfaadsfadse8a","active":"jdnc_1_btorange210113_2","joinnum":"1"}
|
||||
## 助力码获取可以通过 js jd_get_share_code now 命令获取
|
||||
## 注意:京喜农场 种植种子发生变化的时候,互助码也会变!!
|
||||
MyJxnc1=''
|
||||
MyJxnc2=''
|
||||
MyJxnc3=''
|
||||
MyJxnc4=''
|
||||
MyJxnc5=''
|
||||
MyJxnc6=''
|
||||
MyJxncA=''
|
||||
MyJxncB=''
|
||||
|
||||
ForOtherJxnc1=""
|
||||
ForOtherJxnc2=""
|
||||
ForOtherJxnc3=""
|
||||
ForOtherJxnc4=""
|
||||
ForOtherJxnc5=""
|
||||
ForOtherJxnc6=""
|
||||
|
||||
|
||||
################################## 定义京喜农场TOKEN(选填) ##################################
|
||||
## 如果某个Cookie的账号种植的是app种子,则必须填入有效的TOKEN;而种植非app种子则不需要TOKEN
|
||||
## TOKEN的形式:{"farm_jstoken":"749a90f871adsfads8ffda7bf3b1576760","timestamp":"1610165423873","phoneid":"42c7e3dadfadsfdsaac-18f0e4f4a0cf"}
|
||||
## 因TOKEN中带有双引号,因此,变量值两侧必须由一对单引号引起来
|
||||
## TOKEN如何获取请阅读以下文件的注释:https://github.com/LXK9301/jd_scripts/blob/master/jd_jxnc.js
|
||||
TokenJxnc1=''
|
||||
TokenJxnc2=''
|
||||
TokenJxnc3=''
|
||||
TokenJxnc4=''
|
||||
TokenJxnc5=''
|
||||
TokenJxnc6=''
|
||||
|
||||
|
||||
################################## 定义口袋书店互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyBookShop1=""
|
||||
MyBookShop2=""
|
||||
MyBookShop3=""
|
||||
MyBookShop4=""
|
||||
MyBookShop5=""
|
||||
MyBookShop6=""
|
||||
MyBookShopA=""
|
||||
MyBookShopB=""
|
||||
|
||||
ForOtherBookShop1=""
|
||||
ForOtherBookShop2=""
|
||||
ForOtherBookShop3=""
|
||||
ForOtherBookShop4=""
|
||||
ForOtherBookShop5=""
|
||||
ForOtherBookShop6=""
|
||||
|
||||
|
||||
################################## 定义签到领现金互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyCash1=""
|
||||
MyCash2=""
|
||||
MyCash3=""
|
||||
MyCash4=""
|
||||
MyCash5=""
|
||||
MyCash6=""
|
||||
MyCashA=""
|
||||
MyCashB=""
|
||||
|
||||
ForOtherCash1=""
|
||||
ForOtherCash2=""
|
||||
ForOtherCash3=""
|
||||
ForOtherCash4=""
|
||||
ForOtherCash5=""
|
||||
ForOtherCash6=""
|
||||
|
||||
|
||||
################################## 定义闪购盲盒互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MySgmh1=""
|
||||
MySgmh2=""
|
||||
MySgmh3=""
|
||||
MySgmh4=""
|
||||
MySgmh5=""
|
||||
MySgmh6=""
|
||||
MySgmhA=""
|
||||
MySgmhB=""
|
||||
|
||||
ForOtherSgmh1=""
|
||||
ForOtherSgmh2=""
|
||||
ForOtherSgmh3=""
|
||||
ForOtherSgmh4=""
|
||||
ForOtherSgmh5=""
|
||||
ForOtherSgmh6=""
|
||||
|
||||
|
||||
################################## 定义京东环球挑战赛活动互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
## 仅2021年2月22日(含)前有效
|
||||
MyGLOBAL1=""
|
||||
MyGLOBAL2=""
|
||||
MyGLOBAL3=""
|
||||
MyGLOBAL4=""
|
||||
MyGLOBAL5=""
|
||||
MyGLOBAL6=""
|
||||
MyGLOBALA=""
|
||||
MyGLOBALB=""
|
||||
|
||||
ForOtherGLOBAL1=""
|
||||
ForOtherGLOBAL2=""
|
||||
ForOtherGLOBAL3=""
|
||||
ForOtherGLOBAL4=""
|
||||
ForOtherGLOBAL5=""
|
||||
ForOtherGLOBAL6=""
|
||||
|
||||
################################## 定义惊喜财富岛互助(选填) ##################################
|
||||
## 具体填法及要求详见本文件最下方“互助码填法示例”
|
||||
MyJdcfd1=""
|
||||
MyJdcfd2=""
|
||||
MyJdcfd3=""
|
||||
MyJdcfd4=""
|
||||
MyJdcfd5=""
|
||||
MyJdcfd6=""
|
||||
MyJdcfdA=""
|
||||
MyJdcfdB=""
|
||||
|
||||
|
||||
ForOtherJdcfd1=""
|
||||
ForOtherJdcfd2=""
|
||||
ForOtherJdcfd3=""
|
||||
ForOtherJdcfd4=""
|
||||
ForOtherJdcfd5=""
|
||||
ForOtherJdcfd6=""
|
||||
|
||||
################################## 定义东东超市蓝币兑换数量(选填) ##################################
|
||||
## 东东超市蓝币兑换,可用值包括:
|
||||
## 一、0:表示不兑换京豆,这也是js脚本的默认值
|
||||
## 二、20:表示兑换20个京豆
|
||||
## 三、1000:表示兑换1000个京豆
|
||||
## 四、可兑换清单的商品名称,输入能跟唯一识别出来的关键词即可,比如:MARKET_COIN_TO_BEANS="抽纸"
|
||||
## 注意:有些比较贵的实物商品京东只是展示出来忽悠人的,即使你零点用脚本去抢,也会提示没有或提示已下架
|
||||
export MARKET_COIN_TO_BEANS="0"
|
||||
|
||||
|
||||
################################## 定义东东超市蓝币成功兑换奖品是否静默运行(选填) ##################################
|
||||
## 默认 "false" 关闭(即:奖品兑换成功后会发出通知提示),如需要静默运行不发出通知,请改为 "true"
|
||||
export MARKET_REWARD_NOTIFY=""
|
||||
|
||||
|
||||
################################## 定义东东超市是否自动升级商品和货架(选填) ##################################
|
||||
## 升级顺序:解锁升级商品、升级货架,默认 "true" 自动升级,如需关闭自动升级,请改为 "false"
|
||||
export SUPERMARKET_UPGRADE=""
|
||||
|
||||
|
||||
################################## 定义东东超市是否自动更换商圈(选填) ##################################
|
||||
## 小于对方300热力值自动更换商圈队伍,默认 "true" 自动更换,如不想更换商圈,请改为 "false"
|
||||
## 目前已无此功能,申明了也无效
|
||||
# export BUSINESS_CIRCLE_JUMP=""
|
||||
|
||||
|
||||
################################## 定义东东超市是否自动使用金币去抽奖(选填) ##################################
|
||||
## 是否用金币去抽奖,默认 "false" 关闭,如需开启,请修改为 "true"
|
||||
export SUPERMARKET_LOTTERY=""
|
||||
|
||||
|
||||
################################## 定义东东超市是否自动参加PK队伍(选填) ##################################
|
||||
## 是否每次PK活动参加脚本作者创建的PK队伍,"true" 表示参加,"false" 表示不参加,默认为 "true"
|
||||
export JOIN_PK_TEAM=""
|
||||
|
||||
|
||||
################################## 定义东东农场是否静默运行(选填) ##################################
|
||||
## 默认为 "false",不静默,发送推送通知消息,如不想收到通知,请修改为 "true"
|
||||
## 如果你不想完全关闭或者完全开启通知,只想在特定的时间发送通知,可以参考下面的“定义东东萌宠是否静默运行”部分,设定几个if判断条件
|
||||
export FRUIT_NOTIFY_CONTROL=""
|
||||
|
||||
|
||||
################################## 定义东东农场是否使用水滴换豆卡(选填) ##################################
|
||||
## 如果出现限时活动时100g水换20豆,此时比浇水划算,"true" 表示换豆(不浇水),"false" 表示不换豆(继续浇水),默认是"false"
|
||||
## 如需切换为换豆(不浇水),请修改为 "true"
|
||||
export FRUIT_BEAN_CARD=""
|
||||
|
||||
|
||||
################################## 定义宠汪汪喂食克数(选填) ##################################
|
||||
## 你期望的宠汪汪每次喂食克数,只能填入10、20、40、80,默认为10
|
||||
## 如实际持有食物量小于所设置的克数,脚本会自动降一档,直到降无可降
|
||||
## 具体情况请自行在宠汪汪游戏中去查阅攻略
|
||||
export JOY_FEED_COUNT=""
|
||||
|
||||
|
||||
################################## 定义宠汪汪兑换京豆数量(选填) ##################################
|
||||
## 目前的可用值包括:0、20、500、1000,其中0表示为不自动兑换京豆,如不设置,将默认为"20"
|
||||
## 不同等级可兑换不同数量的京豆,详情请见宠汪汪游戏中兑换京豆选项
|
||||
## 500、1000的京豆每天有总量限制,设置了并且你也有足够积分时,也并不代表就一定能抢到
|
||||
export JD_JOY_REWARD_NAME=""
|
||||
|
||||
|
||||
################################## 定义宠汪汪兑换京豆是否静默运行(选填) ##################################
|
||||
## 默认为 "false",在成功兑换京豆时将发送推送通知消息(失败不发送),如想要静默不发送通知,请修改为 "true"
|
||||
export JD_JOY_REWARD_NOTIFY=""
|
||||
|
||||
|
||||
################################## 定义宠汪汪是否自动给好友的汪汪喂食(选填) ##################################
|
||||
## 默认 "false" 不会自动给好友的汪汪喂食,如想自动喂食,请改成 "true"
|
||||
export JOY_HELP_FEED=""
|
||||
|
||||
|
||||
################################## 定义宠汪汪是否自动报名宠物赛跑(选填) ##################################
|
||||
## 默认 "true" 参加宠物赛跑,如需关闭,请改成 "false"
|
||||
export JOY_RUN_FLAG=""
|
||||
|
||||
|
||||
################################## 定义宠汪汪参加比赛类型(选填) ##################################
|
||||
## 当JOY_RUN_FLAG不设置或设置为 "true" 时生效
|
||||
## 可选值:2,10,50,其他值不可以。其中2代表参加双人PK赛,10代表参加10人突围赛,50代表参加50人挑战赛,不填时默认为2
|
||||
## 各个账号间请使用 & 分隔,比如:JOY_TEAM_LEVEL="2&2&50&10"
|
||||
## 如果你有5个账号但只写了四个数字,那么第5个账号将默认参加2人赛,账号如果更多,与此类似
|
||||
export JOY_TEAM_LEVEL=""
|
||||
|
||||
|
||||
################################## 定义宠汪汪赛跑获胜后是否推送通知(选填) ##################################
|
||||
## 控制jd_joy.js脚本宠汪汪赛跑获胜后是否推送通知,"false" 为否(不推送通知消息),"true" 为是(即:发送推送通知消息),默认为 "true"
|
||||
export JOY_RUN_NOTIFY=""
|
||||
|
||||
|
||||
################################## 定义摇钱树是否自动将金果卖出变成金币(选填) ##################################
|
||||
## 金币有时效,默认为 "false",不卖出金果为金币,如想希望自动卖出,请修改为 "true"
|
||||
export MONEY_TREE_SELL_FRUIT=""
|
||||
|
||||
|
||||
################################## 定义东东萌宠是否静默运行(选填) ##################################
|
||||
## 默认 "false"(不静默,发送推送通知消息),如想静默请修改为 true
|
||||
## 每次执行脚本通知太频繁了,改成只在周三和周六中午那一次运行时发送通知提醒
|
||||
## 除掉上述提及时间之外,均设置为 true,静默不发通知
|
||||
## 特别说明:针对北京时间有效。
|
||||
if [ $(date "+%w") -eq 6 ] && [ $(date "+%H") -ge 9 ] && [ $(date "+%H") -lt 14 ]; then
|
||||
export PET_NOTIFY_CONTROL="false"
|
||||
elif [ $(date "+%w") -eq 3 ] && [ $(date "+%H") -ge 9 ] && [ $(date "+%H") -lt 14 ]; then
|
||||
export PET_NOTIFY_CONTROL="false"
|
||||
else
|
||||
export PET_NOTIFY_CONTROL="true"
|
||||
fi
|
||||
|
||||
|
||||
################################## 定义京喜工厂控制哪个京东账号不运行此脚本(选填) ##################################
|
||||
## 输入"1"代表第一个京东账号不运行,多个使用 & 连接,例:"1&3" 代表账号1和账号3不运行京喜工厂脚本,注:输入"0",代表全部账号不运行京喜工厂脚本
|
||||
## 如果使用了 “临时屏蔽某个Cookie” TempBlockCookie 功能,编号会发生变化
|
||||
export DREAMFACTORY_FORBID_ACCOUNT=""
|
||||
|
||||
|
||||
################################## 定义东东工厂控制哪个京东账号不运行此脚本(选填) ##################################
|
||||
## 输入"1"代表第一个京东账号不运行,多个使用 & 连接,例:"1&3" 代表账号1和账号3不运行东东工厂脚本,注:输入"0",代表全部账号不运行东东工厂脚本
|
||||
## 如果使用了 “临时屏蔽某个Cookie” TempBlockCookie 功能,编号会发生变化
|
||||
export JDFACTORY_FORBID_ACCOUNT=""
|
||||
|
||||
|
||||
################################## 定义东东工厂心仪的商品(选填) ##################################
|
||||
## 只有在满足以下条件时,才自动投入电力:一是存储的电力满足生产商品所需的电力,二是心仪的商品有库存,如果没有输入心仪的商品,那么当前你正在生产的商品视作心仪的商品
|
||||
## 如果你看不懂上面的话,请去东东工厂游戏中查阅攻略
|
||||
## 心仪的商品请输入商品的全称或能唯一识别出该商品的关键字
|
||||
export FACTORAY_WANTPRODUCT_NAME=""
|
||||
|
||||
|
||||
################################## 定义京喜农场通知级别(选填) ##################################
|
||||
## 可用值: 0(不通知); 1(本次获得水滴>0); 2(任务执行); 3(任务执行+未种植种子),默认为"3"
|
||||
export JXNC_NOTIFY_LEVEL="3"
|
||||
|
||||
|
||||
################################## 定义取关参数(选填) ##################################
|
||||
## jd_unsubscribe这个任务是用来取关每天做任务关注的商品和店铺,默认在每次运行时取关20个商品和20个店铺
|
||||
## 如果取关数量不够,可以根据情况增加,还可以设置 jdUnsubscribeStopGoods 和 jdUnsubscribeStopShop
|
||||
## 商品取关数量
|
||||
goodPageSize=""
|
||||
## 店铺取关数量
|
||||
shopPageSize=""
|
||||
## 遇到此商品不再取关此商品以及它后面的商品,需去商品详情页长按拷贝商品信息
|
||||
jdUnsubscribeStopGoods=""
|
||||
## 遇到此店铺不再取关此店铺以及它后面的店铺,请从头开始输入店铺名称
|
||||
jdUnsubscribeStopShop=""
|
||||
|
||||
|
||||
################################## 疯狂的JOY(选填) ##################################
|
||||
## 疯狂的JOY循环助力,"true" 表示循环助力,"false" 表示不循环助力,默认 "false"
|
||||
export JDJOY_HELPSELF=""
|
||||
|
||||
## 疯狂的JOY京豆兑换,目前最小值为500/1000京豆,默认为 "0" 不开启京豆兑换
|
||||
export JDJOY_APPLYJDBEAN=""
|
||||
|
||||
## 疯狂的JOY自动购买什么等级的JOY,如需要使用请自行解除注释
|
||||
# export BUY_JOY_LEVEL=""
|
||||
|
||||
|
||||
################################## 定义是否自动加购物车(选填) ##################################
|
||||
## 口袋书店和东东小窝有些任务需要将商品加进购物车才能完成,默认 "false" 不做这些任务,如想做,请设置为 "true"
|
||||
export PURCHASE_SHOPS=""
|
||||
|
||||
################################## 定义京喜财富岛通知(选填) ##################################
|
||||
## 输入true为通知,默认 "false",不填则为不通知
|
||||
export CFD_NOTIFY_CONTROL=""
|
||||
|
||||
|
||||
################################## 定义京小兑是否自动把抽奖卷兑换为兑币,(选填) ##################################
|
||||
## 输入true为自动兑换,不填则为不兑换
|
||||
export JD_JXD_EXCHANGE=""
|
||||
|
||||
|
||||
################################## 定义宠汪汪是否开启内部互助,(选填) ##################################
|
||||
## 输入true为开启内部互助,不填默认关闭
|
||||
export JOY_RUN_HELP_MYSELF=""
|
||||
|
||||
################################## Telegram 代理(选填) ##################################
|
||||
## Telegram 代理的 IP,代理类型为 http,比如你代理是 http://127.0.0.1:1080,则填写 "127.0.0.1"
|
||||
## 如需使用,请自行解除下一行的注释
|
||||
# export TG_PROXY_HOST=""
|
||||
|
||||
## Telegram 代理的端口,代理类型为 http,比如你代理是 http://127.0.0.1:1080,则填写 "1080"
|
||||
## 如需使用,请自行解除下一行的注释
|
||||
# export TG_PROXY_PORT=""
|
||||
|
||||
|
||||
################################## 是否添加DIY脚本(选填) ##################################
|
||||
## 如果你自己会写shell脚本,并且希望在每次git_pull.sh这个脚本运行时,额外运行你的DIY脚本,请赋值为 "true"
|
||||
## 同时,请务必将你的脚本命名为 extra.sh (只能叫这个文件名),放在 config 目录下
|
||||
## 仓库下已经上传extra.sh模板,如果你想使用;可以参考本仓库下 extra.sh 文件;地址:https://raw.githubusercontent.com/dockere/jd-base/master/sample/diy.sh
|
||||
EnableExtraShell=""
|
||||
|
||||
|
||||
################################## 互助码填法示例 ##################################
|
||||
## **互助码是填在My系列变量中的,ForOther系统变量中只要填入My系列的变量名即可,按注释中的例子拼接,以东东农场为例,如下所示。**
|
||||
## **实际上东东农场一个账号只能给别人助力3次,我多写的话,只有前几个会被助力。但如果前面的账号获得的助力次数已经达到上限了,那么还是会尝试继续给余下的账号助力,所以多填也是有意义的。**
|
||||
## **ForOther系列变量必须从1开始编号,依次编下去。**
|
||||
|
||||
# MyFruit1="e6e04602d5e343258873af1651b603ec" # 这是Cookie1这个账号的互助码
|
||||
# MyFruit2="52801b06ce2a462f95e1d59d7e856ef4" # 这是Cookie2这个账号的互助码
|
||||
# MyFruit3="e2fd1311229146cc9507528d0b054da8" # 这是Cookie3这个账号的互助码
|
||||
# MyFruit4="6dc9461f662d490991a31b798f624128" # 这是Cookie4这个账号的互助码
|
||||
# MyFruit5="30f29addd75d44e88fb452bbfe9f2110" # 这是Cookie5这个账号的互助码
|
||||
# MyFruit6="1d02fc9e0e574b4fa928e84cb1c5e70b" # 这是Cookie6这个账号的互助码
|
||||
# MyFruitA="5bc73a365ff74a559bdee785ea97fcc5" # 这是我和别人交换互助,另外一个用户A的互助码
|
||||
# MyFruitB="6d402dcfae1043fba7b519e0d6579a6f" # 这是我和别人交换互助,另外一个用户B的互助码
|
||||
# MyFruitC="5efc7fdbb8e0436f8694c4c393359576" # 这是我和别人交换互助,另外一个用户C的互助码
|
||||
|
||||
# ForOtherFruit1="${MyFruit2}@${MyFruitB}@${MyFruit4}" # Cookie1这个账号助力Cookie2的账号的账号、Cookie4的账号以及用户B
|
||||
# ForOtherFruit2="${MyFruit1}@${MyFruitA}@${MyFruit4}" # Cookie2这个账号助力Cookie1的账号的账号、Cookie4的账号以及用户A
|
||||
# ForOtherFruit3="${MyFruit1}@${MyFruit2}@${MyFruitC}@${MyFruit4}@${MyFruitA}@${MyFruit6}" # 解释同上,东东农场实际上只能助力3次
|
||||
# ForOtherFruit4="${MyFruit1}@${MyFruit2}@${MyFruit3}@${MyFruitC}@${MyFruit6}@${MyFruitA}" # 解释同上,东东农场实际上只能助力3次
|
||||
# ForOtherFruit5="${MyFruit1}@${MyFruit2}@${MyFruit3}@${MyFruitB}@${MyFruit4}@${MyFruit6}@${MyFruitC}@${MyFruitA}"
|
||||
# ForOtherFruit6="${MyFruit1}@${MyFruit2}@${MyFruit3}@${MyFruitA}@${MyFruit4}@${MyFruit5}@${MyFruitC}"
|
||||
@@ -1,27 +0,0 @@
|
||||
# 更新shell及面板代码
|
||||
55 2,13 * * * git_pull >> ${QL_DIR}/log/git_pull.log 2>&1
|
||||
|
||||
# 更新你需要的仓库的代码
|
||||
# diy test作者 test仓库 test路径1|test路径2 黑名单1文件名|黑名单2文件名
|
||||
33 * * * * diy whyour hundun "quanx/jx|quanx/jd" tokens >> ${QL_DIR}/log/diy_pull.log 2>&1
|
||||
31 * * * * diy monk-coder dust i-chenzhe >> ${QL_DIR}/log/diy_pull.log 2>&1
|
||||
|
||||
# 重新build面板
|
||||
# 30 7 */7 * * rebuild >> ${QL_DIR}/log/rebuild.log 2>&1
|
||||
|
||||
# 删除 RmLogDaysAgo 指定天数以前的旧日志,本行为不记录日志
|
||||
57 13 * * * rm_log >/dev/null 2>&1
|
||||
|
||||
# 导出所有互助码清单,日志在log/export_sharecodes下
|
||||
48 5 * * * export_sharecodes
|
||||
|
||||
# 重启挂机脚本
|
||||
# 33 3 * * * js hangup
|
||||
|
||||
# 重置密码
|
||||
# 33 6 */7 * * js resetpwd
|
||||
|
||||
# 运行所有脚本(慎用)
|
||||
# 33 9 */7 * * js runall
|
||||
|
||||
# 其他定时任务
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 添加你需要重启自动执行的任意命令,比如 ql repo
|
||||
## 安装node依赖使用 pnpm install -g xxx xxx
|
||||
## 安装python依赖使用 pip3 install xxx
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,540 @@
|
||||
#!/usr/bin/env python3
|
||||
# _*_ coding:utf-8 _*_
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
|
||||
# 原先的 print 函数和主线程的锁
|
||||
_print = print
|
||||
mutex = threading.Lock()
|
||||
|
||||
|
||||
# 定义新的 print 函数
|
||||
def print(text, *args, **kw):
|
||||
"""
|
||||
使输出有序进行,不出现多线程同一时间输出导致错乱的问题。
|
||||
"""
|
||||
with mutex:
|
||||
_print(text, *args, **kw)
|
||||
|
||||
|
||||
# 通知服务
|
||||
# fmt: off
|
||||
push_config = {
|
||||
'HITOKOTO': False, # 启用一言(随机句子)
|
||||
|
||||
'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
|
||||
'BARK_ARCHIVE': '', # bark 推送是否存档
|
||||
'BARK_GROUP': '', # bark 推送分组
|
||||
'BARK_SOUND': '', # bark 推送声音
|
||||
|
||||
'CONSOLE': True, # 控制台输出
|
||||
|
||||
'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
|
||||
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
|
||||
|
||||
'FSKEY': '', # 飞书机器人的 FSKEY
|
||||
|
||||
'GOBOT_URL': '', # go-cqhttp
|
||||
# 推送到个人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 版
|
||||
|
||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||
|
||||
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
|
||||
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
||||
|
||||
'QYWX_AM': '', # 企业微信应用
|
||||
|
||||
'QYWX_KEY': '', # 企业微信机器人
|
||||
|
||||
'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ
|
||||
'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1434078534
|
||||
'TG_API_HOST': '', # tg 代理 api
|
||||
'TG_PROXY_AUTH': '', # tg 代理认证参数
|
||||
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
|
||||
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
|
||||
}
|
||||
notify_function = []
|
||||
# fmt: on
|
||||
|
||||
# 首先读取 面板变量 或者 github action 运行变量
|
||||
for k in push_config:
|
||||
if os.getenv(k):
|
||||
v = os.getenv(k)
|
||||
push_config[k] = v
|
||||
|
||||
|
||||
def bark(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 bark 推送消息。
|
||||
"""
|
||||
if not push_config.get("BARK_PUSH"):
|
||||
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
|
||||
return
|
||||
print("bark 服务启动")
|
||||
|
||||
if push_config.get("BARK_PUSH").startswith("http"):
|
||||
url = f'{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
|
||||
else:
|
||||
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",
|
||||
}
|
||||
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()
|
||||
|
||||
if response["code"] == 200:
|
||||
print("bark 推送成功!")
|
||||
else:
|
||||
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
|
||||
print("钉钉机器人 服务启动")
|
||||
|
||||
timestamp = str(round(time.time() * 1000))
|
||||
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
|
||||
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
|
||||
string_to_sign_enc = string_to_sign.encode("utf-8")
|
||||
hmac_code = hmac.new(
|
||||
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
|
||||
).digest()
|
||||
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
||||
url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get("DD_BOT_TOKEN")}×tamp={timestamp}&sign={sign}'
|
||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||
response = requests.post(
|
||||
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||
).json()
|
||||
|
||||
if not response["errcode"]:
|
||||
print("钉钉机器人 推送成功!")
|
||||
else:
|
||||
print("钉钉机器人 推送失败!")
|
||||
|
||||
|
||||
def feishu_bot(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 飞书机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("FSKEY"):
|
||||
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("飞书 服务启动")
|
||||
|
||||
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
|
||||
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
|
||||
response = requests.post(url, data=json.dumps(data)).json()
|
||||
|
||||
if response.get("StatusCode") == 0:
|
||||
print("飞书 推送成功!")
|
||||
else:
|
||||
print("飞书 推送失败!错误信息如下:\n", response)
|
||||
|
||||
|
||||
def go_cqhttp(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 go_cqhttp 推送消息。
|
||||
"""
|
||||
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
|
||||
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
|
||||
return
|
||||
print("go-cqhttp 服务启动")
|
||||
|
||||
url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
|
||||
response = requests.get(url).json()
|
||||
|
||||
if response["status"] == "ok":
|
||||
print("go-cqhttp 推送成功!")
|
||||
else:
|
||||
print("go-cqhttp 推送失败!")
|
||||
|
||||
|
||||
def gotify(title:str,content:str) -> None:
|
||||
"""
|
||||
使用 gotify 推送消息。
|
||||
"""
|
||||
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
||||
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("gotify 服务启动")
|
||||
|
||||
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
|
||||
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
|
||||
response = requests.post(url,data=data).json()
|
||||
|
||||
if response.get("id"):
|
||||
print("gotify 推送成功!")
|
||||
else:
|
||||
print("gotify 推送失败!")
|
||||
|
||||
|
||||
def iGot(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 iGot 推送消息。
|
||||
"""
|
||||
if not push_config.get("IGOT_PUSH_KEY"):
|
||||
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("iGot 服务启动")
|
||||
|
||||
url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
|
||||
data = {"title": title, "content": content}
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
response = requests.post(url, data=data, headers=headers).json()
|
||||
|
||||
if response["ret"] == 0:
|
||||
print("iGot 推送成功!")
|
||||
else:
|
||||
print(f'iGot 推送失败!{response["errMsg"]}')
|
||||
|
||||
|
||||
def serverJ(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 serverJ 推送消息。
|
||||
"""
|
||||
if not push_config.get("PUSH_KEY"):
|
||||
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("serverJ 服务启动")
|
||||
|
||||
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
||||
if push_config.get("PUSH_KEY").index("SCT") != -1:
|
||||
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||
else:
|
||||
url = f'https://sc.ftqq.com/${push_config.get("PUSH_KEY")}.send'
|
||||
response = requests.post(url, data=data).json()
|
||||
|
||||
if response.get("errno") == 0 or response.get("code") == 0:
|
||||
print("serverJ 推送成功!")
|
||||
else:
|
||||
print(f'serverJ 推送失败!错误码:{response["message"]}')
|
||||
|
||||
|
||||
def 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 = {
|
||||
"token": push_config.get("PUSH_PLUS_TOKEN"),
|
||||
"title": title,
|
||||
"content": content,
|
||||
"topic": push_config.get("PUSH_PLUS_USER"),
|
||||
}
|
||||
body = json.dumps(data).encode(encoding="utf-8")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(url=url, data=body, headers=headers).json()
|
||||
|
||||
if response["code"] == 200:
|
||||
print("PUSHPLUS 推送成功!")
|
||||
|
||||
else:
|
||||
|
||||
url_old = "http://pushplus.hxtrip.com/send"
|
||||
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
|
||||
print("qmsg 服务启动")
|
||||
|
||||
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
|
||||
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
|
||||
response = requests.post(url=url, params=payload).json()
|
||||
|
||||
if response["code"] == 0:
|
||||
print("qmsg 推送成功!")
|
||||
else:
|
||||
print(f'qmsg 推送失败!{response["reason"]}')
|
||||
|
||||
|
||||
def wecom_app(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 企业微信 APP 推送消息。
|
||||
"""
|
||||
if not push_config.get("QYWX_AM"):
|
||||
print("QYWX_AM 未设置!!\n取消推送")
|
||||
return
|
||||
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": title,
|
||||
"thumb_media_id": media_id,
|
||||
"author": "Author",
|
||||
"content_source_url": "",
|
||||
"content": message.replace("\n", "<br/>"),
|
||||
"digest": message,
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
send_msges = bytes(json.dumps(send_values), "utf-8")
|
||||
respone = requests.post(send_url, send_msges)
|
||||
respone = respone.json()
|
||||
return respone["errmsg"]
|
||||
|
||||
|
||||
def wecom_bot(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 企业微信机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("QYWX_KEY"):
|
||||
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("企业微信机器人服务启动")
|
||||
|
||||
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
|
||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||
response = requests.post(
|
||||
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||
).json()
|
||||
|
||||
if response["errcode"] == 0:
|
||||
print("企业微信机器人推送成功!")
|
||||
else:
|
||||
print("企业微信机器人推送失败!")
|
||||
|
||||
|
||||
def telegram_bot(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 telegram 机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
|
||||
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
|
||||
return
|
||||
print("tg 服务启动")
|
||||
|
||||
if push_config.get("TG_API_HOST"):
|
||||
url = f"https://{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||
else:
|
||||
url = (
|
||||
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||
)
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
payload = {
|
||||
"chat_id": str(push_config.get("TG_USER_ID")),
|
||||
"text": f"{title}\n\n{content}",
|
||||
"disable_web_page_preview": "true",
|
||||
}
|
||||
proxies = None
|
||||
if push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"):
|
||||
if push_config.get("TG_PROXY_AUTH") is not None and "@" not in push_config.get(
|
||||
"TG_PROXY_HOST"
|
||||
):
|
||||
push_config["TG_PROXY_HOST"] = (
|
||||
push_config.get("TG_PROXY_AUTH")
|
||||
+ "@"
|
||||
+ push_config.get("TG_PROXY_HOST")
|
||||
)
|
||||
proxyStr = "http://{}:{}".format(
|
||||
push_config.get("TG_PROXY_HOST"), push_config.get("TG_PROXY_PORT")
|
||||
)
|
||||
proxies = {"http": proxyStr, "https": proxyStr}
|
||||
response = requests.post(
|
||||
url=url, headers=headers, params=payload, proxies=proxies
|
||||
).json()
|
||||
|
||||
if response["ok"]:
|
||||
print("tg 推送成功!")
|
||||
else:
|
||||
print("tg 推送失败!")
|
||||
|
||||
|
||||
def one() -> str:
|
||||
"""
|
||||
获取一条一言。
|
||||
:return:
|
||||
"""
|
||||
url = "https://v1.hitokoto.cn/"
|
||||
res = requests.get(url).json()
|
||||
return res["hitokoto"] + " ----" + res["from"]
|
||||
|
||||
|
||||
if push_config.get("BARK_PUSH"):
|
||||
notify_function.append(bark)
|
||||
if push_config.get("CONSOLE"):
|
||||
notify_function.append(console)
|
||||
if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
|
||||
notify_function.append(dingding_bot)
|
||||
if push_config.get("FSKEY"):
|
||||
notify_function.append(feishu_bot)
|
||||
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
|
||||
notify_function.append(go_cqhttp)
|
||||
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
|
||||
notify_function.append(gotify)
|
||||
if push_config.get("IGOT_PUSH_KEY"):
|
||||
notify_function.append(iGot)
|
||||
if push_config.get("PUSH_KEY"):
|
||||
notify_function.append(serverJ)
|
||||
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||
notify_function.append(pushplus_bot)
|
||||
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||
notify_function.append(qmsg_bot)
|
||||
if push_config.get("QYWX_AM"):
|
||||
notify_function.append(wecom_app)
|
||||
if push_config.get("QYWX_KEY"):
|
||||
notify_function.append(wecom_bot)
|
||||
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
|
||||
notify_function.append(telegram_bot)
|
||||
|
||||
|
||||
def send(title: str, content: str) -> None:
|
||||
if not content:
|
||||
print(f"{title} 推送内容为空!")
|
||||
return
|
||||
|
||||
hitokoto = push_config.get("HITOKOTO")
|
||||
|
||||
text = one() if hitokoto else ""
|
||||
content += "\n\n" + text
|
||||
|
||||
ts = [
|
||||
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
|
||||
for mode in notify_function
|
||||
]
|
||||
[t.start() for t in ts]
|
||||
[t.join() for t in ts]
|
||||
|
||||
|
||||
def main():
|
||||
send("title", "content")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "dependence",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.0.0",
|
||||
"download": "^8.0.0",
|
||||
"http-server": "^0.12.3",
|
||||
"got": "^11.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"request": "^2.88.2",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"tunnel": "0.0.6",
|
||||
"ws": "^7.4.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#!/usr/bin/env bash
|
||||
+168
-59
@@ -1,70 +1,179 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
get_token() {
|
||||
token=$(cat $AuthConf | jq -r .token)
|
||||
}
|
||||
|
||||
get_json_value() {
|
||||
local json=$1
|
||||
local key=$2
|
||||
|
||||
if [[ -z "$3" ]]; then
|
||||
local num=1
|
||||
else
|
||||
local num=$3
|
||||
fi
|
||||
|
||||
local value=$(echo "${json}" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'${key}'\042/){print $(i+1)}}}' | tr -d '"' | sed -n ${num}p)
|
||||
|
||||
echo ${value}
|
||||
token=$(cat $file_auth_user | jq -r .token)
|
||||
}
|
||||
|
||||
add_cron_api() {
|
||||
local currentTimeStamp=$(date +%s)
|
||||
if [ $# -eq 1 ]; then
|
||||
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
||||
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
||||
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
||||
else
|
||||
local schedule=$1
|
||||
local command=$2
|
||||
local name=$3
|
||||
fi
|
||||
local currentTimeStamp=$(date +%s)
|
||||
if [[ $# -eq 1 ]]; then
|
||||
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
||||
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
||||
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
||||
else
|
||||
local schedule=$1
|
||||
local command=$2
|
||||
local name=$3
|
||||
fi
|
||||
|
||||
local api=$(curl "http://localhost:5600/api/crons?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://localhost:5700" \
|
||||
-H "Referer: http://localhost:5700/crontab" \
|
||||
-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\"}" \
|
||||
--compressed)
|
||||
code=$(echo $api | jq -r .code)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "$name 添加成功"
|
||||
else
|
||||
echo -e "$name 添加失败"
|
||||
fi
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?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" \
|
||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo $api | jq -r .code)
|
||||
message=$(echo $api | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "$name -> 添加成功"
|
||||
else
|
||||
echo -e "$name -> 添加失败(${message})"
|
||||
fi
|
||||
}
|
||||
|
||||
update_cron_api() {
|
||||
local currentTimeStamp=$(date +%s)
|
||||
if [[ $# -eq 1 ]]; then
|
||||
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
||||
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
||||
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
||||
local id=$(echo "$1" | awk -F ":" '{print $4}')
|
||||
else
|
||||
local schedule=$1
|
||||
local command=$2
|
||||
local name=$3
|
||||
local id=$4
|
||||
fi
|
||||
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?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 "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"_id\":\"$id\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo $api | jq -r .code)
|
||||
message=$(echo $api | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "$name -> 更新成功"
|
||||
else
|
||||
echo -e "$name -> 更新失败(${message})"
|
||||
fi
|
||||
}
|
||||
|
||||
update_cron_command_api() {
|
||||
local currentTimeStamp=$(date +%s)
|
||||
if [[ $# -eq 1 ]]; then
|
||||
local command=$(echo "$1" | awk -F ":" '{print $1}')
|
||||
local id=$(echo "$1" | awk -F ":" '{print $2}')
|
||||
else
|
||||
local command=$1
|
||||
local id=$2
|
||||
fi
|
||||
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?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 "{\"command\":\"$command\",\"_id\":\"$id\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo $api | jq -r .code)
|
||||
message=$(echo $api | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "$command -> 更新成功"
|
||||
else
|
||||
echo -e "$command -> 更新失败(${message})"
|
||||
fi
|
||||
}
|
||||
|
||||
del_cron_api() {
|
||||
local id=$1
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(curl "http://localhost:5600/api/crons/$id?t=$currentTimeStamp" \
|
||||
-X 'DELETE' \
|
||||
-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://localhost:5700" \
|
||||
-H "Referer: http://localhost:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7")
|
||||
code=$(echo $api | jq -r .code)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "$name 删除成功"
|
||||
else
|
||||
echo -e "$name 删除失败"
|
||||
fi
|
||||
local ids=$1
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
|
||||
-X 'DELETE' \
|
||||
-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 "[$ids]" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo $api | jq -r .code)
|
||||
message=$(echo $api | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "成功"
|
||||
else
|
||||
echo -e "失败(${message})"
|
||||
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() {
|
||||
local ids="$1"
|
||||
local status="$2"
|
||||
local pid="${3:-''}"
|
||||
local logPath="$4"
|
||||
local lastExecutingTime="${5:-0}"
|
||||
local runningTime="${6:-0}"
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?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 "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo $api | jq -r .code)
|
||||
message=$(echo $api | jq -r .message)
|
||||
if [[ $code != 200 ]]; then
|
||||
echo -e "\n## 更新任务状态失败(${message})\n" >> $log_path
|
||||
fi
|
||||
}
|
||||
|
||||
get_token
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 导入通用变量与函数
|
||||
dir_shell=/ql/shell
|
||||
. $dir_shell/share.sh
|
||||
url="${github_proxy_url}https://github.com/SuMaiKaDe/bot.git"
|
||||
repo_path="${dir_repo}/dockerbot"
|
||||
|
||||
echo -e "\n1、安装bot依赖...\n"
|
||||
apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
|
||||
echo -e "\nbot依赖安装成功...\n"
|
||||
|
||||
echo -e "2、下载bot所需文件...\n"
|
||||
if [[ ! -d ${repo_path}/.git ]]; then
|
||||
rm -rf ${repo_path}
|
||||
git_clone_scripts ${url} ${repo_path} "main"
|
||||
fi
|
||||
|
||||
cp -rf "$repo_path/jbot" $dir_root
|
||||
if [[ ! -f "$dir_root/config/bot.json" ]]; then
|
||||
cp -f "$repo_path/config/bot.json" "$dir_root/config"
|
||||
fi
|
||||
echo -e "\nbot文件下载成功...\n"
|
||||
|
||||
echo -e "3、安装python3依赖...\n"
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
|
||||
|
||||
cd $dir_root
|
||||
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 "4、启动bot程序...\n"
|
||||
make_dir $dir_log/bot
|
||||
cd $dir_root
|
||||
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 &
|
||||
echo -e "bot启动成功...\n"
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
dir_shell=/ql/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
reset_env() {
|
||||
echo -e "---> 1. 开始检测配置文件\n"
|
||||
fix_config
|
||||
echo -e "---> 配置文件检测完成\n"
|
||||
|
||||
echo -e "---> 2. 开始安装青龙依赖\n"
|
||||
rm -rf $dir_root/node_modules
|
||||
npm_install_2 $dir_root
|
||||
echo -e "---> 青龙依赖安装完成\n"
|
||||
|
||||
echo -e "---> 3. 开始安装脚本依赖\n"
|
||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||
rm -rf $dir_scripts/node_modules
|
||||
npm_install_2 $dir_scripts
|
||||
echo -e "---> 脚本依赖安装完成\n"
|
||||
}
|
||||
|
||||
copy_dep() {
|
||||
echo -e "---> 1. 复制通知文件\n"
|
||||
echo -e "---> 复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
||||
cp -fv $file_notify_py_sample $file_notify_py
|
||||
echo
|
||||
echo -e "---> 复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
||||
cp -fv $file_notify_js_sample $file_notify_js
|
||||
echo -e "---> 通知文件复制完成\n"
|
||||
|
||||
echo -e "---> 2. 复制nginx配置文件\n"
|
||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||
echo -e "---> 配置文件复制完成\n"
|
||||
}
|
||||
|
||||
reload_pm2() {
|
||||
pm2 l &>/dev/null
|
||||
|
||||
pm2 delete panel --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||
|
||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||
}
|
||||
|
||||
pm2_log() {
|
||||
echo -e "---> pm2日志"
|
||||
local panelOut="/root/.pm2/logs/panel-out.log"
|
||||
local panelError="/root/.pm2/logs/panel-error.log"
|
||||
tail -n 20 "$panelOut"
|
||||
tail -n 20 "$panelError"
|
||||
}
|
||||
|
||||
check_nginx() {
|
||||
local nginxPid=$(ps -ef | grep nginx | grep -v grep)
|
||||
echo -e "=====> 检测nginx服务\n$nginxPid"
|
||||
if [[ $nginxPid ]]; then
|
||||
echo -e "\n=====> nginx服务正常\n"
|
||||
nginx -s reload
|
||||
else
|
||||
echo -e "\n=====> nginx服务异常,重新启动nginx\n"
|
||||
nginx -c /etc/nginx/nginx.conf
|
||||
fi
|
||||
}
|
||||
|
||||
check_ql() {
|
||||
local api=$(curl -s --noproxy "*" "http://0.0.0.0:5700")
|
||||
echo -e "\n=====> 检测面板\n\n$api\n"
|
||||
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
|
||||
echo -e "=====> 面板服务启动正常\n"
|
||||
else
|
||||
echo -e "=====> 面板服务异常,重置基础环境\n"
|
||||
reset_env
|
||||
fi
|
||||
}
|
||||
|
||||
check_pm2() {
|
||||
pm2_log
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
|
||||
-H 'Accept: */*' \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
|
||||
-H 'Referer: http://0.0.0.0:5700/crontab' \
|
||||
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
|
||||
--compressed
|
||||
)
|
||||
echo -e "\n=====> 检测后台\n\n$api\n"
|
||||
if [[ $api =~ "{\"code\"" ]]; then
|
||||
echo -e "=====> 后台服务启动正常\n"
|
||||
else
|
||||
echo -e "=====> 后台服务异常,重置基础环境并重启后台\n"
|
||||
reset_env
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
echo -e "=====> 开始检测"
|
||||
copy_dep
|
||||
check_ql
|
||||
check_nginx
|
||||
check_pm2
|
||||
reload_pm2
|
||||
echo -e "\n=====> 检测结束\n"
|
||||
}
|
||||
|
||||
main
|
||||
@@ -1,141 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 路径、环境判断
|
||||
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
|
||||
LogDir=${ShellDir}/log
|
||||
ConfigDir=${ShellDir}/config
|
||||
FileConf=${ConfigDir}/config.sh
|
||||
CookieConf=${ConfigDir}/cookie.sh
|
||||
[[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] && Opt="P" || Opt="E"
|
||||
Tips="从日志中未找到任何互助码"
|
||||
|
||||
## 所有有互助码的活动,只需要把脚本名称去掉前缀 jd_ 后列在 Name1 中,将其中文名称列在 Name2 中,对应 config.sh 中互助码后缀列在 Name3 中即可。
|
||||
## Name1、Name2 和 Name3 中的三个名称必须一一对应。
|
||||
Name1=(fruit pet plantBean dreamFactory jdfactory crazy_joy jdzz jxnc bookshop cash sgmh cfd global)
|
||||
Name2=(东东农场 东东萌宠 京东种豆得豆 京喜工厂 东东工厂 crazyJoy任务 京东赚赚 京喜农场 口袋书店 签到领现金 闪购盲盒 京喜财富岛 环球挑战赛)
|
||||
Name3=(Fruit Pet Bean DreamFactory JdFactory Joy Jdzz Jxnc BookShop Cash Sgmh Cfd Global)
|
||||
|
||||
|
||||
## 导入 config.sh
|
||||
function Import_Conf {
|
||||
if [ -f ${FileConf} ]
|
||||
then
|
||||
. ${CookieConf}
|
||||
. ${FileConf}
|
||||
if [ ! -s ${CookieConf} ]; then
|
||||
echo -e "请先在Cookie管理中添加一条Cookie...\n"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "配置文件 ${FileConf} 不存在,请先按教程配置好该文件...\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
## 用户数量 UserSum
|
||||
function Count_UserSum {
|
||||
UserSum=$(awk '{print NR}' ${CookieConf} | tail -n1)
|
||||
}
|
||||
|
||||
|
||||
## 导出互助码的通用程序
|
||||
function Cat_Scodes {
|
||||
if [ -d ${LogDir}/jd_$1 ] && [[ $(ls ${LogDir}/jd_$1) != "" ]]; then
|
||||
cd ${LogDir}/jd_$1
|
||||
|
||||
## 导出助力码变量(My)
|
||||
for log in $(ls -r); do
|
||||
case $# in
|
||||
2)
|
||||
codes=$(cat ${log} | grep -${Opt} "开始【京东账号|您的(好友)?助力码为" | uniq | perl -0777 -pe "{s|\*||g; s|开始||g; s|\n您的(好友)?助力码为(:)?:?|:|g; s|,.+||g}" | sed -r "s/【京东账号/My$2/;s/】.*?:/='/;s/】.*?/='/;s/$/'/;s/\(每次运行都变化,不影响\)//")
|
||||
;;
|
||||
3)
|
||||
codes=$(grep -${Opt} $3 ${log} | uniq | sed -r "s/【京东账号/My$2/;s/(.*?】/='/;s/$/'/")
|
||||
;;
|
||||
esac
|
||||
if [[ ${codes} ]]; then
|
||||
## 添加判断,若未找到该用户互助码,则设置为空值
|
||||
for ((user_num=1;user_num<=${UserSum};user_num++)); do
|
||||
echo -e "${codes}" | grep -${Opt}q "My$2${user_num}="
|
||||
if [ $? -eq 1 ]; then
|
||||
if [ $user_num == 1 ]; then
|
||||
codes=$(echo "${codes}" | sed -r "1i My${2}1=''")
|
||||
else
|
||||
codes=$(echo "${codes}" | sed -r "/My$2$(expr ${user_num} - 1)=/a\My$2${user_num}=''")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
## 导出为他人助力变量(ForOther)
|
||||
if [[ ${codes} ]]; then
|
||||
help_code=""
|
||||
for ((user_num=1;user_num<=${UserSum};user_num++)); do
|
||||
echo -e "${codes}" | grep -${Opt}q "My$2${user_num}=''"
|
||||
if [ $? -eq 1 ]; then
|
||||
help_code=${help_code}"\${My"$2${user_num}"}@"
|
||||
fi
|
||||
done
|
||||
## 生成互助规则模板
|
||||
for_other_codes=""
|
||||
case $HelpType in
|
||||
0) ### 统一优先级助力模板
|
||||
new_code=$(echo ${help_code} | sed "s/@$//")
|
||||
for ((user_num=1;user_num<=${UserSum};user_num++)); do
|
||||
if [ $user_num == 1 ]; then
|
||||
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\""${new_code}"\"\n"
|
||||
else
|
||||
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\"\${ForOther"${2}1"}\"\n"
|
||||
fi
|
||||
done
|
||||
;;
|
||||
1) ### 均匀助力模板
|
||||
for ((user_num=1;user_num<=${UserSum};user_num++)); do
|
||||
echo ${help_code} | grep "\${My"$2${user_num}"}@" > /dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
left_str=$(echo ${help_code} | sed "s/\${My$2${user_num}}@/ /g" | awk '{print $1}')
|
||||
right_str=$(echo ${help_code} | sed "s/\${My$2${user_num}}@/ /g" | awk '{print $2}')
|
||||
mark="\${My$2${user_num}}@"
|
||||
else
|
||||
left_str=$(echo ${help_code} | sed "s/${mark}/ /g" | awk '{print $1}')${mark}
|
||||
right_str=$(echo ${help_code} | sed "s/${mark}/ /g" | awk '{print $2}')
|
||||
fi
|
||||
new_code=$(echo ${right_str}${left_str} | sed "s/@$//")
|
||||
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\""${new_code}"\"\n"
|
||||
done
|
||||
;;
|
||||
*) ### 普通优先级助力模板
|
||||
for ((user_num=1;user_num<=${UserSum};user_num++)); do
|
||||
new_code=$(echo ${help_code} | sed "s/\${My"$2${user_num}"}@//;s/@$//")
|
||||
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\""${new_code}"\"\n"
|
||||
done
|
||||
;;
|
||||
esac
|
||||
echo -e "${codes}\n\n${for_other_codes}" | sed s/[[:space:]]//g
|
||||
else
|
||||
echo ${Tips}
|
||||
fi
|
||||
else
|
||||
echo "未运行过 jd_$1 脚本,未产生日志"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
## 汇总
|
||||
function Cat_All {
|
||||
echo -e "\n从最后一个日志提取互助码,受日志内容影响,仅供参考。"
|
||||
for ((i=0; i<${#Name1[*]}; i++)); do
|
||||
echo -e "\n${Name2[i]}:"
|
||||
[[ $(Cat_Scodes "${Name1[i]}" "${Name3[i]}" "的${Name2[i]}好友互助码") == ${Tips} ]] && Cat_Scodes "${Name1[i]}" "${Name3[i]}" || Cat_Scodes "${Name1[i]}" "${Name3[i]}" "的${Name2[i]}好友互助码"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
## 执行并写入日志
|
||||
LogTime=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
LogFile="${LogDir}/export_sharecodes/${LogTime}.log"
|
||||
[ ! -d "${LogDir}/export_sharecodes" ] && mkdir -p ${LogDir}/export_sharecodes
|
||||
Import_Conf && Count_UserSum && Cat_All | perl -pe "{s|京东种豆|种豆|; s|crazyJoy任务|疯狂的JOY|}" | tee ${LogFile}
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#author:spark thanks to: https://github.com/sparkssssssss/scripts
|
||||
|
||||
ShellDir=${QL_DIR:-$(
|
||||
cd $(dirname $0)
|
||||
pwd
|
||||
)}
|
||||
[[ $QL_DIR ]] && ShellJs=js
|
||||
ConfigDir=$ShellDir/config
|
||||
ListCronCurrent=$ConfigDir/crontab.list
|
||||
AuthConf=$ConfigDir/auth.json
|
||||
|
||||
declare -A BlackListDict
|
||||
url=$1
|
||||
path=$2
|
||||
blackword=$3
|
||||
|
||||
if [[ $# -lt 2 ]] || [[ $# -gt 4 ]]; then
|
||||
echo 'Desc: 用户拉取指定用户的指定仓储'
|
||||
echo 'Usage: diy <repourl> <path> <blacklist>'
|
||||
|
||||
echo 'repourl 仓储地址'
|
||||
echo 'path 需要下载脚本的指定目录,多个目录 | 分割 path1 | path2'
|
||||
echo 'blacklist 需要排除的脚本名,多个名称 | 分割 blacklist1 | blacklist2'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
diyscriptsdir=/ql/diyscripts
|
||||
mkdir -p ${diyscriptsdir}
|
||||
|
||||
urlTmp=${url%*/}
|
||||
repoTmp=${urlTmp##*/}
|
||||
repo=${repoTmp%.*}
|
||||
tmp=${url%/*}
|
||||
authorTmp1=${tmp##*/}
|
||||
authorTmp2=${authorTmp1##*:}
|
||||
author=${authorTmp2##*.}
|
||||
|
||||
if [ ! -d "$diyscriptsdir/${author}_${repo}" ]; then
|
||||
echo -e "${author}本地仓库不存在,从远程拉取ing..."
|
||||
cd ${diyscriptsdir} && git clone $url ${author}_${repo}
|
||||
gitpullstatus=$?
|
||||
[ $gitpullstatus -eq 0 ] && echo -e "${author}本地仓库拉取完毕"
|
||||
[ $gitpullstatus -ne 0 ] && echo -e "${author}本地仓库拉取失败,请检查!" && exit 0
|
||||
else
|
||||
cd ${diyscriptsdir}/${author}_${repo}
|
||||
branch=$(git symbolic-ref --short -q HEAD)
|
||||
git fetch --all
|
||||
git reset --hard origin/$branch
|
||||
git pull
|
||||
gitpullstatus=$?
|
||||
fi
|
||||
|
||||
rand() {
|
||||
min=$1
|
||||
max=$(($2 - $min + 1))
|
||||
num=$(cat /proc/sys/kernel/random/uuid | cksum | awk -F ' ' '{print $1}')
|
||||
echo $(($num % $max + $min))
|
||||
}
|
||||
|
||||
addnewcron() {
|
||||
addname=""
|
||||
cd ${diyscriptsdir}/${author}_${repo}
|
||||
express=$(find . -name "*.js")
|
||||
if [ $path ]; then
|
||||
express=$(find . -name "*.js" | egrep $path)
|
||||
fi
|
||||
if [ $blackword ]; then
|
||||
express=$(find . -name "*.js" | egrep -v $blackword | egrep $path)
|
||||
fi
|
||||
for js in $express; do
|
||||
base=$(basename $js)
|
||||
croname=$(echo "${author}_$base" | awk -F\. '{print $1}')
|
||||
script_date=$(cat $js | grep ^[0-9] | awk '{print $1,$2,$3,$4,$5}' | egrep -v "[a-zA-Z]|:|\." | sort | uniq | head -n 1)
|
||||
[ -z "${script_date}" ] && script_date=$(cat $js | grep -Eo "([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9][,-].*)" | sort | uniq | head -n 1)
|
||||
[ -z "${script_date}" ] && cron_min=$(rand 1 59) && cron_hour=$(rand 7 9) && script_date="${cron_min} ${cron_hour} * * *"
|
||||
local oldCron=$(grep -c -w "$croname" "$ListCronCurrent")
|
||||
if [[ oldCron -eq 0 ]]; then
|
||||
local name=$(cat "$js" | grep -E "new Env\(" | perl -pe "s|(^.+)new Env\(\'*\"*(.+?)'*\"*\).+|\2|")
|
||||
add_cron_api "$script_date" "js $croname" "$name"
|
||||
addname="${addname}\n${croname}"
|
||||
echo -e "添加了新的脚本${croname}."
|
||||
fi
|
||||
if [ ! -f "/ql/scripts/${author}_$base" ]; then
|
||||
\cp $js /ql/scripts/${author}_$base
|
||||
else
|
||||
change=$(diff $js /ql/scripts/${author}_$base)
|
||||
[ -n "${change}" ] && \cp $js /ql/scripts/${author}_$base && echo -e "${author}_$base 脚本更新了."
|
||||
fi
|
||||
done
|
||||
[ "$addname" != "" ] && notify "新增 ${author} 自定义脚本" "${addname}"
|
||||
|
||||
}
|
||||
|
||||
delcron() {
|
||||
delname=""
|
||||
cronfiles=$(grep "$author" /ql/config/crontab.list | grep -v "^#" | perl -pe "s|.*ID=(.*) js (${author}_.*)\.*|\1:\2|")
|
||||
for filename in $cronfiles; do
|
||||
local id=$(echo "$1" | awk -F ":" '{print $1}')
|
||||
local name=$(echo "$1" | awk -F ":" '{print $2}')
|
||||
hasFile=$(cd ${diyscriptsdir}/${author}_${repo} && find . -name "$filename.js" | wc -l)
|
||||
if [[ $hasFile != 0 ]]; then
|
||||
del_cron_api "$id"
|
||||
echo -e "删除失效脚本${name}."
|
||||
delname="${delname}\n${author}_${filename}"
|
||||
fi
|
||||
done
|
||||
[ "$delname" != "" ] && notify "删除 ${author} 失效脚本" "${delname}"
|
||||
}
|
||||
|
||||
. $ShellDir/shell/api.sh
|
||||
get_token
|
||||
|
||||
if [[ ${gitpullstatus} -eq 0 ]]; then
|
||||
addnewcron
|
||||
delcron
|
||||
else
|
||||
echo -e "$author 仓库更新失败了."
|
||||
notify "自定义仓库更新失败" "$author"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,297 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ShellDir=${QL_DIR:-$(
|
||||
cd $(dirname $0)
|
||||
pwd
|
||||
)}
|
||||
[[ $QL_DIR ]] && ShellJs=js
|
||||
LogDir=$ShellDir/log
|
||||
DbDir=$ShellDir/db
|
||||
ManualLogDir=$ShellDir/manual_log
|
||||
ScriptsDir=$ShellDir/scripts
|
||||
ConfigDir=$ShellDir/config
|
||||
FileConf=$ConfigDir/config.sh
|
||||
CookieConf=$ConfigDir/cookie.sh
|
||||
AuthConf=$ConfigDir/auth.json
|
||||
ExtraShell=$ConfigDir/extra.sh
|
||||
FileConfSample=$ShellDir/sample/config.sh.sample
|
||||
ListCronSample=$ShellDir/sample/crontab.list.sample
|
||||
ListCronCurrent=$ConfigDir/crontab.list
|
||||
ListCronRemote=$ScriptsDir/docker/crontab_list.sh
|
||||
ListCurrentTask=$LogDir/task.list
|
||||
ListRemoteTask=$LogDir/js.list
|
||||
ListJsAdd=$LogDir/js-add.list
|
||||
ListJsDrop=$LogDir/js-drop.list
|
||||
ContentVersion=$ShellDir/version
|
||||
ContentNewTask=$ShellDir/new_task
|
||||
ContentDropTask=$ShellDir/drop_task
|
||||
SendVersion=$ShellDir/send_version
|
||||
isTermux=$ANDROID_RUNTIME_ROOT$ANDROID_ROOT
|
||||
ShellURL=https://ghproxy.com/https://github.com/whyour/qinglong
|
||||
ScriptsURL=https://ghproxy.com/https://github.com/gossh520/jd_scripts
|
||||
|
||||
Import_Conf() {
|
||||
if [ ! -s $FileConf ]; then
|
||||
echo -e "复制一份 $FileConfSample 示例配置文件\n\n"
|
||||
cp -fv $FileConfSample $FileConf
|
||||
fi
|
||||
if [ ! -s $ListCronCurrent ]; then
|
||||
echo -e "复制一份 $ListCronSample 基础定时任务\n\n"
|
||||
cp -fv $ListCronSample $ListCronCurrent
|
||||
fi
|
||||
[ -f $CookieConf ] && . $CookieConf
|
||||
[ -f $FileConf ] && . $FileConf
|
||||
}
|
||||
|
||||
# 更新shell
|
||||
Git_Pull_Shell() {
|
||||
echo -e "更新shell...\n"
|
||||
cd $ShellDir
|
||||
git remote set-url origin $ShellURL
|
||||
git fetch --all
|
||||
ExitStatusShell=$?
|
||||
git reset --hard origin/master
|
||||
git pull
|
||||
}
|
||||
|
||||
Git_Pull_Shell_Next() {
|
||||
if [[ $ExitStatusShell -eq 0 ]]; then
|
||||
echo -e "更新shell成功...\n"
|
||||
[ ! -d $ShellDir/node_modules ] && Npm_Install panel
|
||||
[ -f $ShellDir/package.json ] && PanelDependNew=$(cat $ShellDir/package.json)
|
||||
[[ "$PanelDependOld" != "$PanelDependNew" ]] && cd $ShellDir && Npm_Install panel
|
||||
cp -f $FileConfSample $ConfigDir/config.sh.sample
|
||||
Notify_Version
|
||||
else
|
||||
echo -e "更新shell失败,请检查原因...\n"
|
||||
fi
|
||||
}
|
||||
|
||||
## npm install
|
||||
Npm_Install() {
|
||||
echo -e "检测到 $1 的依赖包有变化,运行 npm install...\n"
|
||||
Npm_InstallSub
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "\nnpm install 运行不成功,自动删除 $1/node_modules 后再次尝试一遍..."
|
||||
rm -rf node_modules
|
||||
fi
|
||||
echo
|
||||
|
||||
if [ ! -d node_modules ]; then
|
||||
echo -e "运行 npm install...\n"
|
||||
Npm_InstallSub
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "\nnpm install 运行不成功,自动删除 $1/node_modules...\n"
|
||||
echo -e "请进入 $1 目录后手动运行 npm install...\n"
|
||||
echo -e "3...\n"
|
||||
sleep 1
|
||||
echo -e "2...\n"
|
||||
sleep 1
|
||||
echo -e "1...\n"
|
||||
sleep 1
|
||||
rm -rf node_modules
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
## npm install 子程序,判断是否为安卓,判断是否安装有yarn
|
||||
function Npm_InstallSub() {
|
||||
if [ -n "$isTermux" ]; then
|
||||
npm install --production --no-save --no-bin-links || npm install --production --no-bin-links --no-save --registry=https://registry.npm.taobao.org
|
||||
elif ! type yarn >/dev/null 2>&1; then
|
||||
npm install --production --no-save || npm install --production --no-save --registry=https://registry.npm.taobao.org
|
||||
else
|
||||
echo -e "检测到本机安装了 yarn,使用 yarn 替代 npm...\n"
|
||||
yarn install --production --network-timeout 1000000000 || yarn install --production --registry=https://registry.npm.taobao.org --network-timeout 1000000000
|
||||
fi
|
||||
}
|
||||
|
||||
## 检测配置文件版本
|
||||
Notify_Version() {
|
||||
## 识别出两个文件的版本号
|
||||
VerConfSample=$(grep " Version: " $FileConfSample | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
||||
[ -f $FileConf ] && VerConf=$(grep " Version: " $FileConf | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
||||
|
||||
## 删除旧的发送记录文件
|
||||
[ -f "$SendVersion" ] && [[ $(cat $SendVersion) != $VerConfSample ]] && rm -f $SendVersion
|
||||
|
||||
## 识别出更新日期和更新内容
|
||||
UpdateDate=$(grep " Date: " $FileConfSample | awk -F ": " '{print $2}')
|
||||
UpdateContent=$(grep " Update Content: " $FileConfSample | awk -F ": " '{print $2}')
|
||||
|
||||
## 如果是今天,并且版本号不一致,则发送通知
|
||||
if [ -f $FileConf ] && [[ "$VerConf" != "$VerConfSample" ]] && [[ $UpdateDate == $(date "+%Y-%m-%d") ]]; then
|
||||
if [ ! -f $SendVersion ]; then
|
||||
notify "检测到配置文件config.sh.sample有更新" "更新日期: $UpdateDate\n当前版本: $VerConf\n新的版本: $VerConfSample\n更新内容: $UpdateContent\n更新说明: 如需使用新功能请对照config.sh.sample,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。"
|
||||
fi
|
||||
else
|
||||
[ -f $ContentVersion ] && rm -f $ContentVersion
|
||||
[ -f $SendVersion ] && rm -f $SendVersion
|
||||
fi
|
||||
}
|
||||
|
||||
## 每天次数随机,更新时间随机,更新秒数随机,至少6次,至多12次,大部分为8-10次,符合正态分布。
|
||||
Random_Pull_Cron() {
|
||||
if [[ $(date "+%-H") -le 2 ]]; then
|
||||
RanMin=$(($RANDOM % 60))
|
||||
RanSleep=$(($RANDOM % 56))
|
||||
RanHourArray[0]=$(($RANDOM % 3))
|
||||
for ((i = 1; i < 14; i++)); do
|
||||
j=$(($i - 1))
|
||||
tmp=$(($RANDOM % 3 + ${RanHourArray[j]} + 2))
|
||||
[[ $tmp -lt 24 ]] && RanHourArray[i]=$tmp || break
|
||||
done
|
||||
|
||||
RanHour=${RanHourArray[0]}
|
||||
for ((i = 1; i < ${#RanHourArray[*]}; i++)); do
|
||||
RanHour="$RanHour,${RanHourArray[i]}"
|
||||
done
|
||||
|
||||
perl -i -pe "s|.+(git_pull? .+git_pull\.log.*)|$RanMin $RanHour \* \* \* sleep $RanSleep && \1|" $ListCronCurrent
|
||||
crontab $ListCronCurrent
|
||||
fi
|
||||
}
|
||||
|
||||
## 克隆scripts
|
||||
Git_Clone_Scripts() {
|
||||
git clone -b master $ScriptsURL $ScriptsDir
|
||||
ExitStatusScripts=$?
|
||||
}
|
||||
|
||||
## 更新scripts
|
||||
Git_Pull_Scripts() {
|
||||
if [ -d $ScriptsDir/.git ]; then
|
||||
echo -e "更新scripts...\n"
|
||||
cd $ScriptsDir
|
||||
git remote set-url origin $ScriptsURL
|
||||
git fetch --all
|
||||
ExitStatusScripts=$?
|
||||
git reset --hard origin/master
|
||||
git pull
|
||||
else
|
||||
Git_Clone_Scripts
|
||||
fi
|
||||
}
|
||||
|
||||
Git_Pull_Scripts_Next() {
|
||||
if [[ $ExitStatusScripts -eq 0 ]]; then
|
||||
echo -e "更新scripts成功...\n"
|
||||
[ ! -d $ScriptsDir/node_modules ] && Npm_Install scripts
|
||||
[ -f $ScriptsDir/package.json ] && ScriptsDependNew=$(cat $ScriptsDir/package.json)
|
||||
[[ "$ScriptsDependOld" != "$ScriptsDependNew" ]] && cd $ScriptsDir && Npm_Install scripts
|
||||
Diff_Cron
|
||||
if [ -s $ListJsDrop ]; then
|
||||
Output_ListJs $ListJsDrop "失效"
|
||||
Del_Cron
|
||||
fi
|
||||
if [ -s $ListJsAdd ]; then
|
||||
Output_ListJs $ListJsAdd "新"
|
||||
Add_Cron
|
||||
fi
|
||||
else
|
||||
echo -e "更新scripts失败,请检查原因...\n"
|
||||
fi
|
||||
}
|
||||
|
||||
Diff_Cron() {
|
||||
cat $ListCronRemote | grep -E "node.+j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u >$ListRemoteTask
|
||||
cat $ListCronCurrent | grep -E "$ShellJs j[drx]_\w+" | perl -pe "s|.*ID=(.*) $ShellJs (j[drx]_\w+)\.*|\2|" | sort -u >$ListCurrentTask
|
||||
if [ -s $ListCurrentTask ]; then
|
||||
grep -vwf $ListCurrentTask $ListRemoteTask >$ListJsAdd
|
||||
else
|
||||
cp -f $ListRemoteTask $ListJsAdd
|
||||
fi
|
||||
if [ -s $ListRemoteTask ]; then
|
||||
grep -vwf $ListRemoteTask $ListCurrentTask >$ListJsDrop
|
||||
else
|
||||
cp -f $ListCurrentTask $ListJsDrop
|
||||
fi
|
||||
}
|
||||
|
||||
Del_Cron() {
|
||||
if [ $AutoDelCron == true ] && [ -s $ListJsDrop ]; then
|
||||
echo -e "开始尝试自动删除定时任务如下:\n"
|
||||
cat $ListJsDrop
|
||||
echo
|
||||
JsDrop=$(cat $ListJsDrop)
|
||||
for Cron in $JsDrop; do
|
||||
local id=$(cat $ListCronCurrent | grep -E "js $Cron" | perl -pe "s|.*ID=(.*) js $Cron|\1|")
|
||||
del_cron_api "$id"
|
||||
done
|
||||
crontab $ListCronCurrent
|
||||
echo -e "成功删除失效的脚本与定时任务\n"
|
||||
notify "删除 lxk0301 失效脚本" "$JsDrop"
|
||||
fi
|
||||
}
|
||||
|
||||
Add_Cron() {
|
||||
if [ $AutoAddCron == true ] && [ -s $ListJsAdd ]; then
|
||||
echo -e "开始尝试自动添加定时任务\n"
|
||||
JsAdd=$(cat $ListJsAdd)
|
||||
for Cron in $JsAdd; do
|
||||
if [[ $Cron == jd_bean_sign ]]; then
|
||||
local name=$(cat "$ScriptsDir/$Cron.js" | grep -E "new Env\(" | perl -pe "s|(^.+)new Env\(\'*\"*(.+?)'*\"*\).+|\2|")
|
||||
add_cron_api "4 0,9 * * *" "$ShellJs $Cron" "$name"
|
||||
else
|
||||
local name=$(cat "$ScriptsDir/$Cron.js" | grep -E "new Env\(" | perl -pe "s|(^.+)new Env\(\'*\"*(.+?)'*\"*\).+|\2|")
|
||||
local param=$(cat $ListCronRemote | grep -E "\/$Cron\." | perl -pe "s|(^.+) node */scripts/(j[drx]_\w+)\.js.+|\1\:$ShellJs \2|")
|
||||
add_cron_api "$param:$name"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
crontab $ListCronCurrent
|
||||
echo -e "成功添加新的定时任务...\n"
|
||||
notify "新增lxk0301脚本" "$JsAdd"
|
||||
else
|
||||
echo -e "添加新的定时任务出错,请手动添加...\n"
|
||||
notify "尝试自动添加lxk0301以下新的定时任务出错,请手动添加:" "$JsAdd"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
## 输出定时任务变化
|
||||
Output_ListJs() {
|
||||
local list=$1
|
||||
local type=$2
|
||||
if [ -s $list ]; then
|
||||
echo -e "检测到有$type的定时任务:\n"
|
||||
cat $list
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
#################################################################################################################################
|
||||
echo -e "\n--------------------------------------------------------------\n"
|
||||
echo -n "系统时间:"
|
||||
echo $(date "+%Y-%m-%d %H:%M:%S")
|
||||
if [ "${TZ}" = "UTC" ]; then
|
||||
echo -n "北京时间:"
|
||||
echo $(date -d "8 hour" "+%Y-%m-%d %H:%M:%S")
|
||||
fi
|
||||
echo -e "\nJS脚本目录:$ScriptsDir\n"
|
||||
echo -e "--------------------------------------------------------------\n"
|
||||
|
||||
. $ShellDir/shell/api.sh
|
||||
get_token
|
||||
|
||||
Import_Conf
|
||||
|
||||
# 更新shell
|
||||
[ -f $ShellDir/package.json ] && PanelDependOld=$(cat $ShellDir/package.json)
|
||||
Git_Pull_Shell
|
||||
Git_Pull_Shell_Next
|
||||
|
||||
## 更新scripts
|
||||
[ -f $ScriptsDir/package.json ] && ScriptsDependOld=$(cat $ScriptsDir/package.json)
|
||||
Git_Pull_Scripts
|
||||
Git_Pull_Scripts_Next
|
||||
|
||||
## 调用用户自定义的extra.sh
|
||||
if [[ $EnableExtraShell == true ]]; then
|
||||
if [ -f $ExtraShell ]; then
|
||||
. $ExtraShell
|
||||
else
|
||||
echo -e "$ExtraShell 文件不存在,跳过执行DIY脚本...\n"
|
||||
fi
|
||||
fi
|
||||
-283
@@ -1,283 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 路径
|
||||
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
|
||||
[[ ${QL_DIR} ]] && ShellJS=js
|
||||
ScriptsDir=${ShellDir}/scripts
|
||||
ConfigDir=${ShellDir}/config
|
||||
FileConf=${ConfigDir}/config.sh
|
||||
CookieConf=${ConfigDir}/cookie.sh
|
||||
FileConfSample=${ShellDir}/sample/config.sh.sample
|
||||
LogDir=${ShellDir}/log
|
||||
ListScripts=($(cd ${ScriptsDir}; ls *.js | grep -E "j[drx]_"))
|
||||
ListCron=${ConfigDir}/crontab.list
|
||||
ListCronLxk=${ScriptsDir}/docker/crontab_list.sh
|
||||
ListJs=${LogDir}/js.list
|
||||
|
||||
## 导入config.sh
|
||||
function Import_Conf {
|
||||
if [ -f ${FileConf} ]
|
||||
then
|
||||
. ${CookieConf}
|
||||
. ${FileConf}
|
||||
if [[ ! -s ${CookieConf} ]]; then
|
||||
echo -e "请先在Cookie管理中添加一条Cookie...\n"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "配置文件 ${FileConf} 不存在,请先按教程配置好该文件...\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
## 更新crontab
|
||||
function Detect_Cron {
|
||||
if [[ $(cat ${ListCron}) != $(crontab -l) ]]; then
|
||||
crontab ${ListCron}
|
||||
fi
|
||||
}
|
||||
|
||||
## 用户数量UserSum
|
||||
function Count_UserSum {
|
||||
UserSum=0
|
||||
for line in `cat $CookieConf`
|
||||
do
|
||||
((UserSum++))
|
||||
eval Cookie${UserSum}="\"${line}\""
|
||||
done
|
||||
}
|
||||
|
||||
## 组合Cookie和互助码子程序
|
||||
function Combin_Sub {
|
||||
CombinAll=""
|
||||
if [[ ${AutoHelpOther} == true ]] && [[ $1 == ForOther* ]]; then
|
||||
|
||||
ForOtherAll=""
|
||||
MyName=$(echo $1 | perl -pe "s|ForOther|My|")
|
||||
|
||||
for ((m=1; m<=${UserSum}; m++)); do
|
||||
TmpA=${MyName}$m
|
||||
TmpB=${!TmpA}
|
||||
ForOtherAll="${ForOtherAll}@${TmpB}"
|
||||
done
|
||||
|
||||
for ((n=1; n<=${UserSum}; n++)); do
|
||||
for num in ${TempBlockCookie}; do
|
||||
[[ $n -eq $num ]] && continue 2
|
||||
done
|
||||
CombinAll="${CombinAll}&${ForOtherAll}"
|
||||
done
|
||||
|
||||
else
|
||||
for ((i=1; i<=${UserSum}; i++)); do
|
||||
for num in ${TempBlockCookie}; do
|
||||
[[ $i -eq $num ]] && continue 2
|
||||
done
|
||||
Tmp1=$1$i
|
||||
Tmp2=${!Tmp1}
|
||||
CombinAll="${CombinAll}&${Tmp2}"
|
||||
done
|
||||
fi
|
||||
|
||||
echo ${CombinAll} | perl -pe "{s|^&||; s|^@+||; s|&@|&|g; s|@+&|&|g; s|@+|@|g; s|@+$||}"
|
||||
}
|
||||
|
||||
## 组合Cookie、Token与互助码
|
||||
function Combin_All {
|
||||
export JD_COOKIE=$(Combin_Sub Cookie)
|
||||
export FRUITSHARECODES=$(Combin_Sub ForOtherFruit)
|
||||
export PETSHARECODES=$(Combin_Sub ForOtherPet)
|
||||
export PLANT_BEAN_SHARECODES=$(Combin_Sub ForOtherBean)
|
||||
export DREAM_FACTORY_SHARE_CODES=$(Combin_Sub ForOtherDreamFactory)
|
||||
export DDFACTORY_SHARECODES=$(Combin_Sub ForOtherJdFactory)
|
||||
export JDZZ_SHARECODES=$(Combin_Sub ForOtherJdzz)
|
||||
export JDJOY_SHARECODES=$(Combin_Sub ForOtherJoy)
|
||||
export JXNC_SHARECODES=$(Combin_Sub ForOtherJxnc)
|
||||
export JXNCTOKENS=$(Combin_Sub TokenJxnc)
|
||||
export BOOKSHOP_SHARECODES=$(Combin_Sub ForOtherBookShop)
|
||||
export JD_CASH_SHARECODES=$(Combin_Sub ForOtherCash)
|
||||
export JDSGMH_SHARECODES=$(Combin_Sub ForOtherSgmh)
|
||||
export JDCFD_SHARECODES=$(Combin_Sub ForOtherCfd)
|
||||
export JDGLOBAL_SHARECODES=$(Combin_Sub ForOtherGlobal)
|
||||
}
|
||||
|
||||
## 转换JD_BEAN_SIGN_STOP_NOTIFY或JD_BEAN_SIGN_NOTIFY_SIMPLE
|
||||
function Trans_JD_BEAN_SIGN_NOTIFY {
|
||||
case ${NotifyBeanSign} in
|
||||
0)
|
||||
export JD_BEAN_SIGN_STOP_NOTIFY="true"
|
||||
;;
|
||||
1)
|
||||
export JD_BEAN_SIGN_NOTIFY_SIMPLE="true"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
## 转换UN_SUBSCRIBES
|
||||
function Trans_UN_SUBSCRIBES {
|
||||
export UN_SUBSCRIBES="${goodPageSize}\n${shopPageSize}\n${jdUnsubscribeStopGoods}\n${jdUnsubscribeStopShop}"
|
||||
}
|
||||
|
||||
## 申明全部变量
|
||||
function Set_Env {
|
||||
Count_UserSum
|
||||
Combin_All
|
||||
Trans_JD_BEAN_SIGN_NOTIFY
|
||||
Trans_UN_SUBSCRIBES
|
||||
}
|
||||
|
||||
## 随机延迟
|
||||
function Random_Delay {
|
||||
if [[ -n ${RandomDelay} ]] && [[ ${RandomDelay} -gt 0 ]]; then
|
||||
CurMin=$(date "+%-M")
|
||||
if [[ ${CurMin} -gt 2 && ${CurMin} -lt 30 ]] || [[ ${CurMin} -gt 31 && ${CurMin} -lt 59 ]]; then
|
||||
CurDelay=$((${RANDOM} % ${RandomDelay} + 1))
|
||||
echo -e "\n命令未添加 \"now\",随机延迟 ${CurDelay} 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
|
||||
sleep ${CurDelay}
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
## 使用说明
|
||||
function Help {
|
||||
echo -e "本脚本的用法为:"
|
||||
echo -e "1. ${ShellJS} xxx # 如果设置了随机延迟并且当时时间不在0-2、30-31、59分内,将随机延迟一定秒数"
|
||||
echo -e "2. ${ShellJS} xxx now # 无论是否设置了随机延迟,均立即运行"
|
||||
echo -e "3. ${ShellJS} runall # 运行所有非挂机脚本,非常耗时"
|
||||
echo -e "4. ${ShellJS} hangup # 重启挂机程序"
|
||||
echo -e "5. ${ShellJS} resetpwd # 重置控制面板用户名和密码"
|
||||
echo -e "\n针对用法1、用法2中的\"xxx\",可以不输入后缀\".js\",另外,如果前缀是\"jd_\"的话前缀也可以省略。"
|
||||
echo -e "当前有以下脚本可以运行(仅列出以jd_、jr_、jx_开头的脚本):"
|
||||
cd ${ScriptsDir}
|
||||
for ((i=0; i<${#ListScripts[*]}; i++)); do
|
||||
Name=$(grep "new Env" ${ListScripts[i]} | awk -F "'|\"" '{print $2}')
|
||||
echo -e "$(($i + 1)).${Name}:${ListScripts[i]}"
|
||||
done
|
||||
}
|
||||
|
||||
## nohup
|
||||
function Run_Nohup {
|
||||
if [[ $(ps -ef | grep "${js}" | grep -v "grep") != "" ]]; then
|
||||
ps -ef | grep "${js}" | grep -v "grep" | awk '{print $2}' | xargs kill -9
|
||||
fi
|
||||
[ ! -d ${LogDir}/${js} ] && mkdir -p ${LogDir}/${js}
|
||||
LogTime=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
LogFile="${LogDir}/${js}/${LogTime}.log"
|
||||
nohup node ${js}.js > ${LogFile} &
|
||||
}
|
||||
|
||||
## 运行挂机脚本
|
||||
function Run_HangUp {
|
||||
HangUpJs="jd_crazy_joy_coin"
|
||||
cd ${ScriptsDir}
|
||||
for js in ${HangUpJs}; do
|
||||
Import_Conf ${js} && Set_Env
|
||||
if type pm2 >/dev/null 2>&1; then
|
||||
pm2 stop ${js}.js 2>/dev/null
|
||||
pm2 flush
|
||||
pm2 start -a ${js}.js --watch "${ScriptsDir}/${js}.js" --name="${js}"
|
||||
else
|
||||
Run_Nohup >/dev/null 2>&1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
## 重置密码
|
||||
function Reset_Pwd {
|
||||
cp -f ${ShellDir}/sample/auth.json ${ConfigDir}/auth.json
|
||||
echo -e "控制面板重置成功,用户名:admin,密码:adminadmin\n"
|
||||
}
|
||||
|
||||
## 一次性运行所有脚本
|
||||
function Run_All {
|
||||
if [ ! -f ${ListJs} ]; then
|
||||
cat ${ListCronLxk} | grep -E "j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u > ${ListJs}
|
||||
fi
|
||||
echo -e "\n==================== 开始运行所有非挂机脚本 ====================\n"
|
||||
echo -e "请注意:本过程将非常非常耗时,一个账号可能长达几小时,账号越多耗时越长,如果是手动运行,退出终端也将终止运行。\n"
|
||||
echo -e "倒计时5秒...\n"
|
||||
for ((sec=5; sec>0; sec--)); do
|
||||
echo -e "$sec...\n"
|
||||
sleep 1
|
||||
done
|
||||
for file in $(cat ${ListJs}); do
|
||||
echo -e "==================== 运行 $file.js 脚本 ====================\n"
|
||||
${ShellJS} $file now
|
||||
done
|
||||
}
|
||||
|
||||
## 正常运行单个脚本
|
||||
function Run_Normal {
|
||||
Import_Conf $1 && Detect_Cron && Set_Env
|
||||
|
||||
FileNameTmp1=$(echo $1 | perl -pe "s|\.js||")
|
||||
FileNameTmp2=$(echo $1 | perl -pe "{s|jd_||; s|\.js||; s|^|jd_|}")
|
||||
SeekDir="${ScriptsDir} ${ScriptsDir}/backUp ${ConfigDir}"
|
||||
FileName=""
|
||||
WhichDir=""
|
||||
|
||||
for dir in ${SeekDir}
|
||||
do
|
||||
if [ -f ${dir}/${FileNameTmp1}.js ]; then
|
||||
FileName=${FileNameTmp1}
|
||||
WhichDir=${dir}
|
||||
break
|
||||
elif [ -f ${dir}/${FileNameTmp2}.js ]; then
|
||||
FileName=${FileNameTmp2}
|
||||
WhichDir=${dir}
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "${FileName}" ] && [ -n "${WhichDir}" ]
|
||||
then
|
||||
[ $# -eq 1 ] && Random_Delay
|
||||
LogTime=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
LogFile="${LogDir}/${FileName}/${LogTime}.log"
|
||||
[ ! -d ${LogDir}/${FileName} ] && mkdir -p ${LogDir}/${FileName}
|
||||
cd ${WhichDir}
|
||||
node ${FileName}.js 2>&1 | tee ${LogFile}
|
||||
else
|
||||
echo -e "\n在${ScriptsDir}、${ScriptsDir}/backUp、${ConfigDir}三个目录下均未检测到 $1 脚本的存在,请确认...\n"
|
||||
Help
|
||||
fi
|
||||
}
|
||||
|
||||
## 命令检测
|
||||
case $# in
|
||||
0)
|
||||
echo
|
||||
Help
|
||||
;;
|
||||
1)
|
||||
case $1 in
|
||||
hangup)
|
||||
Run_HangUp
|
||||
;;
|
||||
resetpwd)
|
||||
Reset_Pwd
|
||||
;;
|
||||
runall)
|
||||
Run_All
|
||||
;;
|
||||
*)
|
||||
Run_Normal $1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
2)
|
||||
case $2 in
|
||||
now)
|
||||
Run_Normal $1 $2
|
||||
;;
|
||||
*)
|
||||
echo -e "\n命令输入错误...\n"
|
||||
Help
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo -e "\n命令过多...\n"
|
||||
Help
|
||||
;;
|
||||
esac
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const notify = require('/ql/scripts/sendNotify');
|
||||
const notify = require('/ql/scripts/sendNotify.js');
|
||||
const title = process.argv[2];
|
||||
const content = process.argv[3];
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/bin/bash
|
||||
#author:spark thanks to: https://github.com/sparkssssssss/scripts
|
||||
|
||||
. /ql/config/cookie.sh
|
||||
. /ql/config/config.sh
|
||||
title=$(echo $1|sed 's/-/_/g')
|
||||
msg=$(echo -e $2)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
|
||||
|
||||
echo -e "更新shell...\n"
|
||||
cd ${ShellDir}
|
||||
git fetch --all
|
||||
git pull
|
||||
echo -e "更新shell完成...\n"
|
||||
|
||||
echo -e "重新build...\n"
|
||||
yarn install --network-timeout 1000000000 || yarn install --registry=https://registry.npm.taobao.org --network-timeout 1000000000
|
||||
yarn build
|
||||
yarn build-back
|
||||
yarn cache clean
|
||||
echo -e "重新build完成...\n"
|
||||
|
||||
echo -e "重启服务...\n"
|
||||
|
||||
PIDS=`ps -ef|grep "app.js"|grep -v grep`
|
||||
if [ "$PIDS" != "" ]; then
|
||||
pm2 restart panel
|
||||
else
|
||||
pm2 start ${QL_DIR}/build/app.js -n panel
|
||||
fi
|
||||
|
||||
nginx -s reload
|
||||
|
||||
echo -e "重启服务完成...\n"
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 判断环境
|
||||
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
|
||||
LogDir=${ShellDir}/log
|
||||
|
||||
## 导入配置文件
|
||||
. ${ShellDir}/config/cookie.sh
|
||||
. ${ShellDir}/config/config.sh
|
||||
|
||||
## 删除运行js脚本的旧日志
|
||||
function Rm_JsLog {
|
||||
LogFileList=$(ls -l ${LogDir}/*/*.log | awk '{print $9}')
|
||||
for log in ${LogFileList}
|
||||
do
|
||||
LogDate=$(echo ${log} | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
|
||||
if [[ $(uname -s) == Darwin ]]
|
||||
then
|
||||
DiffTime=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "${LogDate}" +%s)))
|
||||
else
|
||||
DiffTime=$(($(date +%s) - $(date +%s -d "${LogDate}")))
|
||||
fi
|
||||
[ ${DiffTime} -gt $((${RmLogDaysAgo} * 86400)) ] && rm -vf ${log}
|
||||
done
|
||||
}
|
||||
|
||||
## 删除git_pull.sh的运行日志
|
||||
function Rm_GitPullLog {
|
||||
if [[ $(uname -s) == Darwin ]]
|
||||
then
|
||||
DateDelLog=$(date -v-${RmLogDaysAgo}d "+%Y-%m-%d")
|
||||
else
|
||||
Stmp=$(($(date "+%s") - 86400 * ${RmLogDaysAgo}))
|
||||
DateDelLog=$(date -d "@${Stmp}" "+%Y-%m-%d")
|
||||
fi
|
||||
LineEndGitPull=$[$(cat ${LogDir}/git_pull.log | grep -n "${DateDelLog} " | head -1 | awk -F ":" '{print $1}') - 3]
|
||||
[ ${LineEndGitPull} -gt 0 ] && perl -i -ne "{print unless 1 .. ${LineEndGitPull} }" ${LogDir}/git_pull.log
|
||||
}
|
||||
|
||||
## 删除空文件夹
|
||||
function Rm_EmptyDir {
|
||||
cd ${LogDir}
|
||||
for dir in $(ls)
|
||||
do
|
||||
if [ -d ${dir} ] && [[ $(ls ${dir}) == "" ]]; then
|
||||
rm -rf ${dir}
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
## 运行
|
||||
if [ -n "${RmLogDaysAgo}" ]; then
|
||||
echo -e "查找旧日志文件中...\n"
|
||||
Rm_JsLog
|
||||
Rm_GitPullLog
|
||||
Rm_EmptyDir
|
||||
echo -e "删除旧日志执行完毕\n"
|
||||
fi
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 导入通用变量与函数
|
||||
dir_shell=/ql/shell
|
||||
. $dir_shell/share.sh
|
||||
|
||||
days=$1
|
||||
|
||||
## 删除运行js脚本的旧日志
|
||||
remove_js_log() {
|
||||
local log_full_path_list=$(find $dir_log/ -name "*.log")
|
||||
local diff_time
|
||||
for log in $log_full_path_list; do
|
||||
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
|
||||
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
|
||||
else
|
||||
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
|
||||
fi
|
||||
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
## 删除空文件夹
|
||||
remove_empty_dir() {
|
||||
cd $dir_log
|
||||
for dir in $(ls); do
|
||||
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
|
||||
rm -rf $dir
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
## 运行
|
||||
if [[ ${days} ]]; then
|
||||
echo -e "查找旧日志文件中...\n"
|
||||
remove_js_log
|
||||
remove_empty_dir
|
||||
echo -e "删除旧日志执行完毕\n"
|
||||
fi
|
||||
Executable
+322
@@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 目录
|
||||
dir_root=/ql
|
||||
dir_shell=$dir_root/shell
|
||||
dir_sample=$dir_root/sample
|
||||
dir_config=$dir_root/config
|
||||
dir_scripts=$dir_root/scripts
|
||||
dir_repo=$dir_root/repo
|
||||
dir_raw=$dir_root/raw
|
||||
dir_log=$dir_root/log
|
||||
dir_db=$dir_root/db
|
||||
dir_dep=$dir_root/deps
|
||||
dir_list_tmp=$dir_log/.tmp
|
||||
dir_code=$dir_log/code
|
||||
dir_update_log=$dir_log/update
|
||||
ql_static_repo=$dir_repo/static
|
||||
|
||||
## 文件
|
||||
file_config_sample=$dir_sample/config.sample.sh
|
||||
file_env=$dir_config/env.sh
|
||||
file_sharecode=$dir_config/sharecode.sh
|
||||
file_config_user=$dir_config/config.sh
|
||||
file_auth_sample=$dir_sample/auth.sample.json
|
||||
file_auth_user=$dir_config/auth.json
|
||||
file_extra_shell=$dir_config/extra.sh
|
||||
file_task_before=$dir_config/task_before.sh
|
||||
file_task_after=$dir_config/task_after.sh
|
||||
file_task_sample=$dir_sample/task.sample.sh
|
||||
file_extra_sample=$dir_sample/extra.sample.sh
|
||||
file_notify_js_sample=$dir_sample/notify.js
|
||||
file_notify_py_sample=$dir_sample/notify.py
|
||||
file_notify_py=$dir_scripts/notify.py
|
||||
file_notify_js=$dir_scripts/sendNotify.js
|
||||
task_error_log_path=$dir_log/task_error.log
|
||||
nginx_app_conf=$dir_root/docker/front.conf
|
||||
nginx_conf=$dir_root/docker/nginx.conf
|
||||
|
||||
## 清单文件
|
||||
list_crontab_user=$dir_config/crontab.list
|
||||
list_crontab_sample=$dir_sample/crontab.sample.list
|
||||
list_own_scripts=$dir_list_tmp/own_scripts.list
|
||||
list_own_user=$dir_list_tmp/own_user.list
|
||||
list_own_add=$dir_list_tmp/own_add.list
|
||||
list_own_drop=$dir_list_tmp/own_drop.list
|
||||
|
||||
## 软连接及其原始文件对应关系
|
||||
link_name=(
|
||||
task
|
||||
ql
|
||||
notify
|
||||
)
|
||||
original_name=(
|
||||
task.sh
|
||||
update.sh
|
||||
notify.sh
|
||||
)
|
||||
|
||||
init_env() {
|
||||
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules
|
||||
}
|
||||
|
||||
import_config() {
|
||||
[[ -f $file_config_user ]] && . $file_config_user
|
||||
[[ -f $file_env ]] && . $file_env
|
||||
|
||||
command_timeout_time=${CommandTimeoutTime:-"1h"}
|
||||
github_proxy_url=${GithubProxyUrl:-""}
|
||||
file_extensions=${RepoFileExtensions:-"js py"}
|
||||
|
||||
if [[ -n "${DefaultCronRule}" ]]; then
|
||||
default_cron="${DefaultCronRule}"
|
||||
else
|
||||
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
||||
fi
|
||||
}
|
||||
|
||||
make_dir() {
|
||||
local dir=$1
|
||||
if [[ ! -d $dir ]]; then
|
||||
mkdir -p $dir
|
||||
fi
|
||||
}
|
||||
|
||||
detect_termux() {
|
||||
if [[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] || [[ $PATH == *com.termux* ]]; then
|
||||
is_termux=1
|
||||
else
|
||||
is_termux=0
|
||||
fi
|
||||
}
|
||||
|
||||
detect_macos() {
|
||||
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
|
||||
}
|
||||
|
||||
gen_random_num() {
|
||||
local divi=$1
|
||||
echo $((${RANDOM} % $divi))
|
||||
}
|
||||
|
||||
link_shell_sub() {
|
||||
local link_path="$1"
|
||||
local original_path="$2"
|
||||
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
|
||||
rm -f $link_path 2>/dev/null
|
||||
ln -sf $original_path $link_path
|
||||
fi
|
||||
}
|
||||
|
||||
link_shell() {
|
||||
if [[ $is_termux -eq 1 ]]; then
|
||||
local path="/data/data/com.termux/files/usr/bin/"
|
||||
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
|
||||
local path="/usr/local/bin/"
|
||||
else
|
||||
local path=""
|
||||
echo -e "脚本功能受限,请自行添加命令的软连接...\n"
|
||||
fi
|
||||
if [[ $path ]]; then
|
||||
for ((i = 0; i < ${#link_name[*]}; i++)); do
|
||||
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
define_cmd() {
|
||||
local cmd_prefix cmd_suffix
|
||||
if type task &>/dev/null; then
|
||||
cmd_suffix=""
|
||||
if [[ -f "$dir_shell/task.sh" ]]; then
|
||||
cmd_prefix=""
|
||||
else
|
||||
cmd_prefix="bash "
|
||||
fi
|
||||
else
|
||||
cmd_suffix=".sh"
|
||||
if [[ -f "$dir_shell/task.sh" ]]; then
|
||||
cmd_prefix="$dir_shell/"
|
||||
else
|
||||
cmd_prefix="bash $dir_shell/"
|
||||
fi
|
||||
fi
|
||||
for ((i = 0; i < ${#link_name[*]}; i++)); do
|
||||
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
|
||||
done
|
||||
}
|
||||
|
||||
fix_config() {
|
||||
make_dir $dir_config
|
||||
make_dir $dir_log
|
||||
make_dir $dir_db
|
||||
make_dir $dir_scripts
|
||||
make_dir $dir_list_tmp
|
||||
make_dir $dir_repo
|
||||
make_dir $dir_raw
|
||||
make_dir $dir_update_log
|
||||
make_dir $dir_dep
|
||||
|
||||
if [[ ! -s $file_config_user ]]; then
|
||||
echo -e "复制一份 $file_config_sample 为 $file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
|
||||
cp -fv $file_config_sample $file_config_user
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -f $file_env ]]; then
|
||||
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
|
||||
touch $file_env
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -f $file_task_before ]]; then
|
||||
echo -e "复制一份 $file_task_sample 为 $file_task_before\n"
|
||||
cp -fv $file_task_sample $file_task_before
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -f $file_task_after ]]; then
|
||||
echo -e "复制一份 $file_task_sample 为 $file_task_after\n"
|
||||
cp -fv $file_task_sample $file_task_after
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -f $file_extra_shell ]]; then
|
||||
echo -e "复制一份 $file_extra_sample 为 $file_extra_shell\n"
|
||||
cp -fv $file_extra_sample $file_extra_shell
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $file_auth_user ]]; then
|
||||
echo -e "复制一份 $file_auth_sample 为 $file_auth_user\n"
|
||||
cp -fv $file_auth_sample $file_auth_user
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $file_notify_py ]]; then
|
||||
echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
||||
cp -fv $file_notify_py_sample $file_notify_py
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $file_notify_js ]]; then
|
||||
echo -e "复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
||||
cp -fv $file_notify_js_sample $file_notify_js
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
|
||||
echo -e "检测到默认nginx配置文件,清空...\n"
|
||||
cat /dev/null > /etc/nginx/conf.d/default.conf
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
npm_install_sub() {
|
||||
if [ $is_termux -eq 1 ]; then
|
||||
npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
|
||||
elif ! type pnpm &>/dev/null; then
|
||||
npm install --production --registry=https://registry.npm.taobao.org || npm install --production
|
||||
else
|
||||
pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
|
||||
fi
|
||||
}
|
||||
|
||||
npm_install_1() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work=$1
|
||||
|
||||
cd $dir_work
|
||||
echo -e "运行 npm install...\n"
|
||||
npm_install_sub
|
||||
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
npm_install_2() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work=$1
|
||||
|
||||
cd $dir_work
|
||||
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
|
||||
npm_install_sub
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
|
||||
npm_install_1 $dir_work
|
||||
fi
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
diff_and_copy() {
|
||||
local copy_source=$1
|
||||
local copy_to=$2
|
||||
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
|
||||
cp -f $copy_source $copy_to
|
||||
fi
|
||||
}
|
||||
|
||||
update_depend() {
|
||||
local dir_current=$(pwd)
|
||||
|
||||
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
|
||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||
npm_install_2 $dir_scripts
|
||||
fi
|
||||
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
git_clone_scripts() {
|
||||
local url=$1
|
||||
local dir=$2
|
||||
local branch=$3
|
||||
[[ $branch ]] && local part_cmd="-b $branch "
|
||||
echo -e "开始克隆仓库 $url 到 $dir\n"
|
||||
git clone $part_cmd $url $dir
|
||||
exit_status=$?
|
||||
}
|
||||
|
||||
git_pull_scripts() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work="$1"
|
||||
local branch="$2"
|
||||
[[ $branch ]] && local part_cmd="origin/${branch}"
|
||||
cd $dir_work
|
||||
echo -e "开始更新仓库:$dir_work\n"
|
||||
git fetch --all
|
||||
exit_status=$?
|
||||
git reset --hard $part_cmd
|
||||
git pull
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
reset_romote_url() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work=$1
|
||||
local url=$2
|
||||
local branch="$3"
|
||||
|
||||
[[ $branch ]] && local part_cmd="origin/${branch}"
|
||||
|
||||
if [[ -d "$dir_work/.git" ]]; then
|
||||
cd $dir_work
|
||||
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
||||
git remote set-url origin $url >/dev/null
|
||||
git reset --hard $part_cmd >/dev/null
|
||||
cd $dir_current
|
||||
fi
|
||||
}
|
||||
|
||||
random_range() {
|
||||
local beg=$1
|
||||
local end=$2
|
||||
echo $((RANDOM % ($end - $beg) + $beg))
|
||||
}
|
||||
|
||||
init_env
|
||||
detect_termux
|
||||
detect_macos
|
||||
define_cmd
|
||||
fix_config
|
||||
|
||||
import_config $1 > $task_error_log_path 2>&1
|
||||
Executable
+341
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 导入通用变量与函数
|
||||
dir_shell=/ql/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
## 选择python3还是node
|
||||
define_program() {
|
||||
local first_param=$1
|
||||
if [[ $first_param == *.js ]]; then
|
||||
which_program="node"
|
||||
elif [[ $first_param == *.py ]]; then
|
||||
which_program="python3"
|
||||
elif [[ $first_param == *.sh ]]; then
|
||||
which_program="bash"
|
||||
elif [[ $first_param == *.ts ]]; then
|
||||
which_program="ts-node-transpile-only"
|
||||
else
|
||||
which_program=""
|
||||
fi
|
||||
}
|
||||
|
||||
random_delay() {
|
||||
local random_delay_max=$RandomDelay
|
||||
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
|
||||
local current_min=$(date "+%-M")
|
||||
if [[ $current_min -ne 0 ]] && [[ $current_min -ne 30 ]]; then
|
||||
delay_second=$(($(gen_random_num $random_delay_max) + 1))
|
||||
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
|
||||
sleep $delay_second
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
## scripts目录下所有可运行脚本数组
|
||||
gen_array_scripts() {
|
||||
local dir_current=$(pwd)
|
||||
local i="-1"
|
||||
cd $dir_scripts
|
||||
for file in $(ls); do
|
||||
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
||||
let i++
|
||||
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
|
||||
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
|
||||
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
|
||||
fi
|
||||
done
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
## 使用说明
|
||||
usage() {
|
||||
define_cmd
|
||||
gen_array_scripts
|
||||
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
|
||||
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
|
||||
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
|
||||
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
|
||||
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
|
||||
if [[ ${#array_scripts[*]} -gt 0 ]]; then
|
||||
echo -e "\n当前有以下脚本可以运行:"
|
||||
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
|
||||
echo -e "$(($i + 1)). ${array_scripts_name[i]}:${array_scripts[i]}"
|
||||
done
|
||||
else
|
||||
echo -e "\n暂无脚本可以执行"
|
||||
fi
|
||||
}
|
||||
|
||||
## run nohup,$1:文件名,不含路径,带后缀
|
||||
run_nohup() {
|
||||
local file_name=$1
|
||||
nohup node $file_name &>$log_path &
|
||||
}
|
||||
|
||||
## 正常运行单个脚本,$1:传入参数
|
||||
run_normal() {
|
||||
local first_param=$1
|
||||
define_program "$first_param"
|
||||
if [[ $first_param == *.js ]]; then
|
||||
if [[ $# -eq 1 ]]; then
|
||||
random_delay
|
||||
fi
|
||||
fi
|
||||
|
||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
log_dir_tmp="${first_param##*/}"
|
||||
log_dir_tmp_path="${first_param%%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd="&>> $log_path"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir "$log_dir"
|
||||
|
||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${first_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
first_param=${first_param/$relative_path\//}
|
||||
fi
|
||||
eval timeout -k 10s $command_timeout_time $which_program $first_param $cmd
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
}
|
||||
|
||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||
run_concurrent() {
|
||||
local first_param="$1"
|
||||
local env_param="$2"
|
||||
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
||||
if [[ ! $env_param ]]; then
|
||||
echo -e "\n 缺少并发运行的环境变量参数"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||
local runArr=($(eval echo $tempArr))
|
||||
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
|
||||
|
||||
local n=0
|
||||
for i in ${runArr[@]}; do
|
||||
array_run[n]=${array[$i - 1]}
|
||||
let n++
|
||||
done
|
||||
|
||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||
|
||||
define_program "$first_param"
|
||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
log_dir_tmp="${first_param##*/}"
|
||||
log_dir_tmp_path="${first_param%%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd="&>> $log_path"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir $log_dir
|
||||
|
||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${first_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
first_param=${first_param/$relative_path\//}
|
||||
fi
|
||||
for i in "${!array[@]}"; do
|
||||
export ${env_param}=${array[i]}
|
||||
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
timeout -k 10s $command_timeout_time $which_program $first_param &>$single_log_path &
|
||||
done
|
||||
|
||||
wait
|
||||
for i in "${!array[@]}"; do
|
||||
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
eval cat $single_log_path $cmd
|
||||
[[ -f $single_log_path ]] && rm -f $single_log_path
|
||||
done
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
}
|
||||
|
||||
run_designated() {
|
||||
local file_param="$1"
|
||||
local env_param="$2"
|
||||
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
||||
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
|
||||
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
define_program "$file_param"
|
||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
log_dir_tmp_path="${file_param%%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd="&>> $log_path"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir $log_dir
|
||||
|
||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||
local runArr=($(eval echo $tempArr))
|
||||
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
|
||||
|
||||
local n=0
|
||||
for i in ${runArr[@]}; do
|
||||
array_run[n]=${array[$i - 1]}
|
||||
let n++
|
||||
done
|
||||
|
||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
eval timeout -k 10s $command_timeout_time $which_program $file_param $cmd
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
}
|
||||
|
||||
## 运行其他命令
|
||||
run_else() {
|
||||
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
local log_dir_tmp="${1##*/}"
|
||||
local log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd="&>> $log_path"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir "$log_dir"
|
||||
|
||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
eval timeout -k 10s $command_timeout_time "$@" $cmd
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
}
|
||||
|
||||
## 命令检测
|
||||
main() {
|
||||
show_log="false"
|
||||
while getopts ":l" opt
|
||||
do
|
||||
case $opt in
|
||||
l)
|
||||
show_log="true"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
||||
|
||||
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
|
||||
case $# in
|
||||
1)
|
||||
run_normal "$1"
|
||||
;;
|
||||
*)
|
||||
case $2 in
|
||||
now)
|
||||
run_normal "$1" "$2"
|
||||
;;
|
||||
conc)
|
||||
run_concurrent "$1" "$3" "$*"
|
||||
;;
|
||||
desi)
|
||||
run_designated "$1" "$3" "$*"
|
||||
;;
|
||||
*)
|
||||
run_else "$@"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
[[ -f $log_path ]] && cat $log_path
|
||||
elif [[ $# -eq 0 ]]; then
|
||||
echo
|
||||
usage
|
||||
else
|
||||
run_else "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
exit 0
|
||||
Executable
+536
@@ -0,0 +1,536 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
dir_shell=/ql/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
send_mark=$dir_shell/send_mark
|
||||
|
||||
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
|
||||
diff_cron() {
|
||||
local list_scripts="$1"
|
||||
local list_task="$2"
|
||||
local list_add="$3"
|
||||
local list_drop="$4"
|
||||
if [[ -s $list_task ]]; then
|
||||
grep -vwf $list_task $list_scripts >$list_add
|
||||
elif [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
|
||||
cp -f $list_scripts $list_add
|
||||
fi
|
||||
if [[ -s $list_scripts ]]; then
|
||||
grep -vwf $list_scripts $list_task >$list_drop
|
||||
else
|
||||
cp -f $list_task $list_drop
|
||||
fi
|
||||
}
|
||||
|
||||
## 检测配置文件版本
|
||||
detect_config_version() {
|
||||
## 识别出两个文件的版本号
|
||||
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
||||
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
||||
|
||||
## 删除旧的发送记录文件
|
||||
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
|
||||
|
||||
## 识别出更新日期和更新内容
|
||||
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
|
||||
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
|
||||
|
||||
## 如果是今天,并且版本号不一致,则发送通知
|
||||
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
|
||||
if [[ ! -f $send_mark ]]; then
|
||||
local notify_title="配置文件更新通知"
|
||||
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
|
||||
echo -e $notify_content
|
||||
notify "$notify_title" "$notify_content"
|
||||
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
|
||||
fi
|
||||
else
|
||||
[[ -f $send_mark ]] && rm -f $send_mark
|
||||
fi
|
||||
}
|
||||
|
||||
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
|
||||
output_list_add_drop() {
|
||||
local list=$1
|
||||
local type=$2
|
||||
if [[ -s $list ]]; then
|
||||
echo -e "检测到有$type的定时任务:\n"
|
||||
cat $list
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true;2.正常更新js脚本,没有报错;3.存在失效任务
|
||||
## $1:失效任务清单文件路径
|
||||
del_cron() {
|
||||
local list_drop=$1
|
||||
local path=$2
|
||||
local detail=""
|
||||
local ids=""
|
||||
echo -e "开始尝试自动删除失效的定时任务...\n"
|
||||
for cron in $(cat $list_drop); do
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
|
||||
if [[ $ids ]]; then
|
||||
ids="$ids,\"$id\""
|
||||
else
|
||||
ids="\"$id\""
|
||||
fi
|
||||
cron_file="$dir_scripts/${cron}"
|
||||
if [[ -f $cron_file ]]; then
|
||||
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
||||
rm -f $cron_file
|
||||
fi
|
||||
[[ -z $cron_name ]] && cron_name="$cron"
|
||||
if [[ $detail ]]; then
|
||||
detail="${detail}\n${cron_name}"
|
||||
else
|
||||
detail="${cron_name}"
|
||||
fi
|
||||
done
|
||||
if [[ $ids ]]; then
|
||||
result=$(del_cron_api "$ids")
|
||||
notify "$path 删除任务${result}" "$detail"
|
||||
fi
|
||||
}
|
||||
|
||||
## 自动增加定时任务,需要:1.AutoAddCron 设置为 true;2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空
|
||||
## $1:新任务清单文件路径
|
||||
add_cron() {
|
||||
local list_add=$1
|
||||
local path=$2
|
||||
echo -e "开始尝试自动添加定时任务...\n"
|
||||
local detail=""
|
||||
cd $dir_scripts
|
||||
for file in $(cat $list_add); do
|
||||
local file_name=${file/${path}\//}
|
||||
file_name=${file_name/${path}\_/}
|
||||
if [[ -f $file ]]; then
|
||||
cron_line=$(
|
||||
perl -ne "{
|
||||
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
|
||||
}" $file |
|
||||
perl -pe "{
|
||||
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g;
|
||||
s|\*([\d\*])(.*)|\1\2|g;
|
||||
s| | |g;
|
||||
}" | sort -u | head -1
|
||||
)
|
||||
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
||||
[[ -z $cron_name ]] && cron_name="$file_name"
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
|
||||
echo -e "$result"
|
||||
if [[ $detail ]]; then
|
||||
detail="${detail}${result}\n"
|
||||
else
|
||||
detail="${result}\n"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
notify "$path 新增任务" "$detail"
|
||||
}
|
||||
|
||||
## 更新仓库
|
||||
update_repo() {
|
||||
local url="$1"
|
||||
local path="$2"
|
||||
local blackword="$3"
|
||||
local dependence="$4"
|
||||
local branch="$5"
|
||||
local tmp="${url%/*}"
|
||||
local authorTmp1="${tmp##*/}"
|
||||
local authorTmp2="${authorTmp1##*:}"
|
||||
local author="${authorTmp2##*.}"
|
||||
|
||||
local repo_path="${dir_repo}/${uniq_path}"
|
||||
|
||||
make_dir "${dir_scripts}/${uniq_path}"
|
||||
|
||||
local formatUrl="$url"
|
||||
if [[ $github_proxy_url ]]; then
|
||||
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
|
||||
[[ ! $url =~ "https://ghproxy.com" ]] && formatUrl="${github_proxy_url}${url}"
|
||||
fi
|
||||
fi
|
||||
if [[ -d ${repo_path}/.git ]]; then
|
||||
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
|
||||
git_pull_scripts ${repo_path} "${branch}"
|
||||
else
|
||||
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
|
||||
fi
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新${repo_path}成功...\n"
|
||||
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
||||
else
|
||||
echo -e "\n更新${repo_path}失败,重新下载全新仓库...\n"
|
||||
rm -rf ${repo_path}
|
||||
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新${repo_path}成功...\n"
|
||||
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
||||
else
|
||||
echo -e "\n更新${repo_path}失败,请检查网络...\n"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
## 更新所有 raw 文件
|
||||
update_raw() {
|
||||
echo -e "--------------------------------------------------------------\n"
|
||||
local url="$1"
|
||||
local raw_url="$url"
|
||||
if [[ $github_proxy_url ]]; then
|
||||
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
|
||||
[[ ! $url =~ "https://ghproxy.com" ]] && raw_url="${github_proxy_url}${url}"
|
||||
fi
|
||||
fi
|
||||
local suffix="${raw_url##*.}"
|
||||
local raw_file_name="${uniq_path}.${suffix}"
|
||||
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
|
||||
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
|
||||
if [[ $? -eq 0 ]]; then
|
||||
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
|
||||
echo -e "下载 ${raw_file_name} 成功...\n"
|
||||
cd $dir_raw
|
||||
local filename="raw_${raw_file_name}"
|
||||
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
|
||||
cp -f $raw_file_name $dir_scripts/${filename}
|
||||
cron_line=$(
|
||||
perl -ne "{
|
||||
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
|
||||
}" $raw_file_name |
|
||||
perl -pe "{
|
||||
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
|
||||
s|\*([\d\*])(.*)|\1\2|g;
|
||||
s| | |g;
|
||||
}" | sort -u | head -1
|
||||
)
|
||||
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
||||
[[ -z $cron_name ]] && cron_name="$raw_file_name"
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||
if [[ -z $cron_id ]]; then
|
||||
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
|
||||
echo -e "$result\n"
|
||||
notify "新增任务通知" "\n$result"
|
||||
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
||||
fi
|
||||
else
|
||||
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
|
||||
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
## 调用用户自定义的extra.sh
|
||||
run_extra_shell() {
|
||||
if [[ ${EnableExtraShell} == true ]]; then
|
||||
if [[ -f $file_extra_shell ]]; then
|
||||
echo -e "--------------------------------------------------------------\n"
|
||||
. $file_extra_shell
|
||||
else
|
||||
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
## 脚本用法
|
||||
usage() {
|
||||
echo -e "本脚本用法:"
|
||||
echo -e "1. $cmd_update update # 更新并重启青龙"
|
||||
echo -e "2. $cmd_update extra # 运行自定义脚本"
|
||||
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
|
||||
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本"
|
||||
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
|
||||
echo -e "6. $cmd_update bot # 启动tg-bot"
|
||||
echo -e "7. $cmd_update check # 检测青龙环境并修复"
|
||||
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
|
||||
echo -e "9. $cmd_update resettfa # 禁用两步登录"
|
||||
}
|
||||
|
||||
## 更新qinglong
|
||||
update_qinglong() {
|
||||
patch_version
|
||||
|
||||
local no_restart="$1"
|
||||
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
|
||||
reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git" "master"
|
||||
git_pull_scripts $dir_root "master"
|
||||
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新$dir_root成功...\n"
|
||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||
detect_config_version
|
||||
update_depend
|
||||
|
||||
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
||||
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
||||
else
|
||||
echo -e "\n更新$dir_root失败,请检查原因...\n"
|
||||
fi
|
||||
|
||||
local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git"
|
||||
if [[ -d ${ql_static_repo}/.git ]]; then
|
||||
reset_romote_url ${ql_static_repo} ${url} "master"
|
||||
git_pull_scripts ${ql_static_repo} "master"
|
||||
else
|
||||
git_clone_scripts ${url} ${ql_static_repo}
|
||||
fi
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新$ql_static_repo成功...\n"
|
||||
local static_version=$(cat /ql/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
|
||||
echo -e "\n当前版本 $static_version...\n"
|
||||
cd $dir_root
|
||||
rm -rf $dir_root/build && rm -rf $dir_root/dist
|
||||
cp -rf $ql_static_repo/* $dir_root
|
||||
if [[ $no_restart != "no-restart" ]]; then
|
||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||
echo -e "重启面板中..."
|
||||
sleep 3
|
||||
reload_pm2
|
||||
fi
|
||||
else
|
||||
echo -e "\n更新$dir_root失败,请检查原因...\n"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
patch_version() {
|
||||
if [[ -f "/ql/db/cookie.db" ]]; then
|
||||
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
||||
mv /ql/db/cookie.db /ql/db/env.db
|
||||
rm -rf /ql/db/cookie.db
|
||||
echo
|
||||
fi
|
||||
|
||||
if ! type ts-node &>/dev/null; then
|
||||
pnpm i -g ts-node typescript tslib
|
||||
fi
|
||||
|
||||
git config --global pull.rebase false
|
||||
}
|
||||
|
||||
reload_pm2() {
|
||||
pm2 l &>/dev/null
|
||||
|
||||
pm2 delete panel --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||
|
||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||
}
|
||||
|
||||
## 对比脚本
|
||||
diff_scripts() {
|
||||
local dir_current=$(pwd)
|
||||
local repo_path="$1"
|
||||
local author="$2"
|
||||
local path="$3"
|
||||
local blackword="$4"
|
||||
local dependence="$5"
|
||||
|
||||
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
||||
|
||||
local list_add="$dir_list_tmp/${uniq_path}_add.list"
|
||||
local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
|
||||
diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
|
||||
|
||||
if [[ -s $list_drop ]]; then
|
||||
output_list_add_drop $list_drop "失效"
|
||||
if [[ ${AutoDelCron} == true ]]; then
|
||||
del_cron $list_drop $uniq_path
|
||||
fi
|
||||
fi
|
||||
if [[ -s $list_add ]]; then
|
||||
output_list_add_drop $list_add "新"
|
||||
if [[ ${AutoAddCron} == true ]]; then
|
||||
add_cron $list_add $uniq_path
|
||||
fi
|
||||
fi
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
## 生成脚本的路径清单文件
|
||||
gen_list_repo() {
|
||||
local dir_current=$(pwd)
|
||||
local repo_path="$1"
|
||||
local author="$2"
|
||||
local path="$3"
|
||||
local blackword="$4"
|
||||
local dependence="$5"
|
||||
|
||||
rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
|
||||
|
||||
cd ${repo_path}
|
||||
|
||||
local cmd="find ."
|
||||
local index=0
|
||||
for extension in $file_extensions; do
|
||||
if [[ $index -eq 0 ]]; then
|
||||
cmd="${cmd} -name \"*.${extension}\""
|
||||
else
|
||||
cmd="${cmd} -o -name \"*.${extension}\""
|
||||
fi
|
||||
let index+=1
|
||||
done
|
||||
files=$(eval $cmd | sed 's/^..//')
|
||||
if [[ $path ]]; then
|
||||
files=$(echo "$files" | egrep $path)
|
||||
fi
|
||||
if [[ $blackword ]]; then
|
||||
files=$(echo "$files" | egrep -v $blackword)
|
||||
fi
|
||||
|
||||
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
|
||||
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
|
||||
|
||||
if [[ $dependence ]]; then
|
||||
cd ${repo_path}
|
||||
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
|
||||
for _file in ${results}; do
|
||||
file_path=$(dirname $_file)
|
||||
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
|
||||
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -d $dir_dep ]]; then
|
||||
cp $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
|
||||
fi
|
||||
|
||||
for file in ${files}; do
|
||||
filename=$(basename $file)
|
||||
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
|
||||
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
|
||||
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
if [[ $cron_id ]]; then
|
||||
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
|
||||
fi
|
||||
done
|
||||
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
get_uniq_path() {
|
||||
local url="$1"
|
||||
local branch="$2"
|
||||
local urlTmp="${url%*/}"
|
||||
local repoTmp="${urlTmp##*/}"
|
||||
local repo="${repoTmp%.*}"
|
||||
local tmp="${url%/*}"
|
||||
local authorTmp1="${tmp##*/}"
|
||||
local authorTmp2="${authorTmp1##*:}"
|
||||
local author="${authorTmp2##*.}"
|
||||
|
||||
uniq_path="${author}_${repo}"
|
||||
[[ $branch ]] && uniq_path="${uniq_path}_${branch}"
|
||||
}
|
||||
|
||||
main() {
|
||||
## for ql update
|
||||
show_log="false"
|
||||
while getopts ":l" opt
|
||||
do
|
||||
case $opt in
|
||||
l)
|
||||
show_log="true"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
||||
|
||||
local p1=$1
|
||||
local p2=$2
|
||||
local p3=$3
|
||||
local p4=$4
|
||||
local p5=$5
|
||||
local p6=$6
|
||||
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
local log_path="$dir_log/update/${log_time}_$p1.log"
|
||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
case $p1 in
|
||||
update)
|
||||
cmd=">> $log_path 2>&1"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
eval echo -e "## 开始执行... $begin_time\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
eval update_qinglong "$2" $cmd
|
||||
;;
|
||||
extra)
|
||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||
run_extra_shell >>$log_path
|
||||
;;
|
||||
repo)
|
||||
get_user_info
|
||||
get_uniq_path "$p2" "$p6"
|
||||
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
|
||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||
if [[ -n $p2 ]]; then
|
||||
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path
|
||||
else
|
||||
echo -e "命令输入错误...\n"
|
||||
usage
|
||||
fi
|
||||
;;
|
||||
raw)
|
||||
get_user_info
|
||||
get_uniq_path "$p2"
|
||||
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
|
||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||
if [[ -n $p2 ]]; then
|
||||
update_raw "$p2" >>$log_path
|
||||
else
|
||||
echo -e "命令输入错误...\n"
|
||||
usage
|
||||
fi
|
||||
;;
|
||||
rmlog)
|
||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||
. $dir_shell/rmlog.sh "$p2" >>$log_path
|
||||
;;
|
||||
bot)
|
||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||
. $dir_shell/bot.sh >>$log_path
|
||||
;;
|
||||
check)
|
||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||
. $dir_shell/check.sh >>$log_path
|
||||
;;
|
||||
resetlet)
|
||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
|
||||
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
|
||||
echo "$auth_value" >$file_auth_user
|
||||
;;
|
||||
resettfa)
|
||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
|
||||
echo -e "禁用两步验证成功 \n $auth_value" >>$log_path
|
||||
echo "$auth_value" >$file_auth_user
|
||||
;;
|
||||
*)
|
||||
echo -e "命令输入错误...\n"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒" >>$log_path
|
||||
cat $log_path
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
exit 0
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import { history } from 'umi';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
|
||||
export function render(oldRender: any) {
|
||||
request
|
||||
.get(`${config.apiPrefix}user`)
|
||||
.then((data) => {
|
||||
if (data.data && data.data.username) {
|
||||
return oldRender();
|
||||
}
|
||||
localStorage.removeItem(config.authKey);
|
||||
history.push('/login');
|
||||
oldRender();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
if (e.response && e.response.status === 401) {
|
||||
localStorage.removeItem(config.authKey);
|
||||
history.push('/login');
|
||||
oldRender();
|
||||
}
|
||||
});
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,7 @@
|
||||
import { PageLoading } from '@ant-design/pro-layout';
|
||||
|
||||
const NewPageLoading = () => {
|
||||
return <PageLoading delay={1}></PageLoading>;
|
||||
};
|
||||
|
||||
export default NewPageLoading;
|
||||
@@ -7,17 +7,24 @@ import {
|
||||
FolderOutlined,
|
||||
RadiusSettingOutlined,
|
||||
ControlOutlined,
|
||||
ContainerOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
export default {
|
||||
route: {
|
||||
routes: [
|
||||
{
|
||||
name: 'login',
|
||||
name: '登录',
|
||||
path: '/login',
|
||||
hideInMenu: true,
|
||||
component: '@/pages/login/index',
|
||||
},
|
||||
{
|
||||
name: '初始化',
|
||||
path: '/initialization',
|
||||
hideInMenu: true,
|
||||
component: '@/pages/initialization/index',
|
||||
},
|
||||
{
|
||||
path: '/crontab',
|
||||
name: '定时任务',
|
||||
@@ -25,10 +32,10 @@ export default {
|
||||
component: '@/pages/crontab/index',
|
||||
},
|
||||
{
|
||||
path: '/cookie',
|
||||
name: 'Cookie管理',
|
||||
path: '/env',
|
||||
name: '环境变量',
|
||||
icon: <RadiusSettingOutlined />,
|
||||
component: '@/pages/cookie/index',
|
||||
component: '@/pages/env/index',
|
||||
},
|
||||
{
|
||||
path: '/config',
|
||||
@@ -37,10 +44,16 @@ export default {
|
||||
component: '@/pages/config/index',
|
||||
},
|
||||
{
|
||||
path: '/diy',
|
||||
name: '自定义脚本',
|
||||
path: '/script',
|
||||
name: '脚本管理',
|
||||
icon: <FormOutlined />,
|
||||
component: '@/pages/diy/index',
|
||||
component: '@/pages/script/index',
|
||||
},
|
||||
{
|
||||
path: '/dependence',
|
||||
name: '依赖管理',
|
||||
icon: <ContainerOutlined />,
|
||||
component: '@/pages/dependence/index',
|
||||
},
|
||||
{
|
||||
path: '/diff',
|
||||
@@ -50,7 +63,7 @@ export default {
|
||||
},
|
||||
{
|
||||
path: '/log',
|
||||
name: '日志',
|
||||
name: '任务日志',
|
||||
icon: <FolderOutlined />,
|
||||
component: '@/pages/log/index',
|
||||
},
|
||||
@@ -62,12 +75,10 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
location: {
|
||||
pathname: '/',
|
||||
},
|
||||
navTheme: 'light',
|
||||
fixSiderbar: true,
|
||||
contentWidth: 'Fixed',
|
||||
splitMenus: false,
|
||||
logo: 'https://qinglong.whyour.cn/qinglong.png',
|
||||
logo: '/images/qinglong.png',
|
||||
siderWidth: 180,
|
||||
} as any;
|
||||
|
||||
+272
-19
@@ -1,15 +1,43 @@
|
||||
body {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
background-color: rgb(248, 248, 248);
|
||||
}
|
||||
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
@import '~@/styles/variable.less';
|
||||
@import '~codemirror/lib/codemirror.css';
|
||||
|
||||
.code-mirror-wrapper .CodeMirror {
|
||||
position: absolute;
|
||||
height: calc(100vh - 128px);
|
||||
width: calc(100% - 32px);
|
||||
@font-face {
|
||||
font-family: 'Source Code Pro';
|
||||
src: url('../assets/fonts/SourceCodePro-Regular.ttf.woff2') format('woff2'),
|
||||
url('../assets/fonts/SourceCodePro-Regular.otf.woff') format('woff'),
|
||||
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.log-modal {
|
||||
.ant-modal {
|
||||
padding-bottom: 0 !important;
|
||||
width: 580px !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: break-spaces;
|
||||
line-height: 17px;
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.monaco-editor:not(.rename-box) {
|
||||
height: calc(100vh - 128px) !important;
|
||||
height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;
|
||||
.view-overlays .current-line {
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rename-box {
|
||||
height: 0;
|
||||
.rename-input {
|
||||
height: 0;
|
||||
padding: 0 !important;
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-grid-content.wide {
|
||||
@@ -17,25 +45,250 @@ body {
|
||||
overflow: auto;
|
||||
.ant-pro-page-container-children-content {
|
||||
overflow: auto;
|
||||
height: calc(100vh - 96px);
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.cookie-wrapper {
|
||||
.ant-table-cell-ellipsis {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.ant-tooltip {
|
||||
max-width: 500px !important;
|
||||
.ant-tooltip-inner {
|
||||
word-break: break-all !important;
|
||||
}
|
||||
}
|
||||
|
||||
.env-wrapper {
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-pro-grid-content.wide {
|
||||
.ant-pro-page-container-children-content {
|
||||
height: calc(100vh - 144px);
|
||||
}
|
||||
.log-wrapper {
|
||||
.log-select {
|
||||
width: 250px;
|
||||
}
|
||||
.code-mirror-wrapper .CodeMirror {
|
||||
height: calc(100vh - 176px);
|
||||
.ant-page-header-heading-left {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.config-wrapper {
|
||||
.config-select {
|
||||
width: 250px;
|
||||
}
|
||||
.ant-page-header-heading-left {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tree {
|
||||
.ant-tree-treenode {
|
||||
width: 100%;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.ant-tree-node-content-wrapper {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-tree {
|
||||
.ant-select-tree-treenode {
|
||||
width: 100%;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.ant-select-tree-node-content-wrapper {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-page-container.ql-container-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 48px);
|
||||
height: calc(100vh - var(--vh-offset, 0px) - 48px);
|
||||
.ant-pro-grid-content.wide {
|
||||
flex: 1;
|
||||
.ant-pro-grid-content-children {
|
||||
height: calc(100% - 36px);
|
||||
> div,
|
||||
.log-container,
|
||||
.react-codemirror2,
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ql-container-wrapper {
|
||||
&.crontab-wrapper,
|
||||
&.log-wrapper,
|
||||
&.env-wrapper,
|
||||
&.config-wrapper {
|
||||
.CodeMirror {
|
||||
width: calc(100vw - 80px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tooltip {
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
|
||||
margin-bottom: 0 !important;
|
||||
min-height: calc(100vh - 66px);
|
||||
min-height: calc(100vh - var(--vh-offset, 0px) - 66px);
|
||||
}
|
||||
|
||||
.Resizer {
|
||||
background: #fff;
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding;
|
||||
background-clip: padding-box;
|
||||
border-left: 5px solid rgba(42, 161, 255, 0);
|
||||
border-right: 5px solid rgba(42, 161, 255, 0);
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
-webkit-transition: all 2s ease;
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
height: 11px;
|
||||
margin: -5px 0;
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 11px;
|
||||
margin: 0 -5px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover,
|
||||
.Resizer.vertical:hover {
|
||||
border-left-color: rgba(42, 161, 255, 0.5);
|
||||
border-right-color: rgba(42, 161, 255, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.edit-modal {
|
||||
.ant-drawer-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-countdown.ant-statistic {
|
||||
display: inline-block;
|
||||
.ant-statistic-content {
|
||||
font-size: 14px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form-item-extra {
|
||||
word-break: break-all;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
[data-dark='true'] {
|
||||
/* Change autocomplete styles in WebKit */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover,
|
||||
textarea:-webkit-autofill:focus,
|
||||
select:-webkit-autofill,
|
||||
select:-webkit-autofill:hover,
|
||||
select:-webkit-autofill:focus {
|
||||
border: 1px solid @border-color-base;
|
||||
box-shadow: none;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
-webkit-text-fill-color: @text-color;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-page-container-children-content {
|
||||
margin: 18px 18px 18px;
|
||||
}
|
||||
|
||||
.ant-pro-basicLayout-content {
|
||||
margin: 18px;
|
||||
.ant-pro-page-container {
|
||||
margin: -18px -18px -18px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-item.ant-pro-sider-collapsed-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.side-menu-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
|
||||
.side-menu-collapse-button:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.side-menu-user-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.side-menu-user-drop-menu {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
text-align: left;
|
||||
background-clip: padding-box;
|
||||
outline: none;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%),
|
||||
0 12px 48px 16px rgb(0 0 0 / 3%);
|
||||
.ant-dropdown-menu-item:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
+271
-38
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ProLayout from '@ant-design/pro-layout';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
|
||||
import {
|
||||
enable as enableDarkMode,
|
||||
disable as disableDarkMode,
|
||||
@@ -8,13 +8,41 @@ import {
|
||||
} from 'darkreader';
|
||||
import defaultProps from './defaultProps';
|
||||
import { Link, history } from 'umi';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import config from '@/utils/config';
|
||||
import 'codemirror/mode/shell/shell.js';
|
||||
import { request } from '@/utils/http';
|
||||
import './index.less';
|
||||
import vhCheck from 'vh-check';
|
||||
import { version, changeLogLink, changeLog } from '../version';
|
||||
import { useCtx, useTheme } from '@/utils/hooks';
|
||||
import { message, Badge, Modal, Avatar, Dropdown, Menu, Popover } from 'antd';
|
||||
// @ts-ignore
|
||||
import SockJS from 'sockjs-client';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://ea2fede373244db99c536210b910d9da@o1051273.ingest.sentry.io/6047851',
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
release: version,
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
export default function (props: any) {
|
||||
const ctx = useCtx();
|
||||
const theme = useTheme();
|
||||
const [user, setUser] = useState<any>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
|
||||
const ws = useRef<any>(null);
|
||||
const [socketMessage, setSocketMessage] = useState<any>();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const logout = () => {
|
||||
request.post(`${config.apiPrefix}logout`).then(() => {
|
||||
localStorage.removeItem(config.authKey);
|
||||
@@ -22,46 +50,213 @@ export default function (props: any) {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const isAuth = localStorage.getItem(config.authKey);
|
||||
if (!isAuth) {
|
||||
history.push('/login');
|
||||
const getSystemInfo = () => {
|
||||
request
|
||||
.get(`${config.apiPrefix}system`)
|
||||
.then(({ code, data }) => {
|
||||
if (code === 200) {
|
||||
setSystemInfo(data);
|
||||
if (!data.isInitialized) {
|
||||
history.push('/initialization');
|
||||
setLoading(false);
|
||||
} else {
|
||||
getUser();
|
||||
}
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
const getUser = (needLoading = true) => {
|
||||
needLoading && setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}user`)
|
||||
.then(({ code, data }) => {
|
||||
if (code === 200 && data.username) {
|
||||
setUser(data);
|
||||
if (props.location.pathname === '/') {
|
||||
history.push('/crontab');
|
||||
}
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
needLoading && setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
const reloadUser = () => {
|
||||
getUser(false);
|
||||
};
|
||||
|
||||
const setTheme = () => {
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const storageTheme = localStorage.getItem('qinglong_dark_theme');
|
||||
const isDark =
|
||||
(media.matches && storageTheme !== 'light') || storageTheme === 'dark';
|
||||
if (isDark) {
|
||||
document.body.setAttribute('data-dark', 'true');
|
||||
} else {
|
||||
document.body.setAttribute('data-dark', 'false');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
vhCheck();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.location.pathname === '/') {
|
||||
history.push('/crontab');
|
||||
if (systemInfo && systemInfo.isInitialized && !user) {
|
||||
getUser();
|
||||
}
|
||||
}, [props.location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
const theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||
if (!systemInfo) {
|
||||
getSystemInfo();
|
||||
}
|
||||
}, [systemInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
setTheme();
|
||||
}, [theme.theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||
setFetchMethod(window.fetch);
|
||||
if (theme === 'dark') {
|
||||
enableDarkMode({
|
||||
brightness: 100,
|
||||
contrast: 90,
|
||||
sepia: 10,
|
||||
});
|
||||
} else if (theme === 'light') {
|
||||
if (_theme === 'dark') {
|
||||
enableDarkMode({});
|
||||
} else if (_theme === 'light') {
|
||||
disableDarkMode();
|
||||
} else {
|
||||
followSystemColorScheme({
|
||||
brightness: 100,
|
||||
contrast: 90,
|
||||
sepia: 10,
|
||||
});
|
||||
followSystemColorScheme({});
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (props.location.pathname === '/login') {
|
||||
return props.children;
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
ws.current = new SockJS(
|
||||
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`,
|
||||
);
|
||||
|
||||
ws.current.onmessage = (e: any) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'ping') {
|
||||
if (data && data.message === 'hanhh') {
|
||||
console.log('websocket连接成功', e);
|
||||
} else {
|
||||
console.log('websocket连接失败', e);
|
||||
}
|
||||
}
|
||||
setSocketMessage(data);
|
||||
} catch (error) {
|
||||
console.log('websocket连接失败', e);
|
||||
}
|
||||
};
|
||||
|
||||
const wsCurrent = ws.current;
|
||||
|
||||
return () => {
|
||||
wsCurrent.close();
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
window.onload = () => {
|
||||
const timing = performance.timing;
|
||||
console.log(`白屏时间: ${timing.responseStart - timing.navigationStart}`);
|
||||
console.log(
|
||||
`请求完毕至DOM加载: ${timing.domInteractive - timing.responseEnd}`,
|
||||
);
|
||||
console.log(
|
||||
`解释dom树耗时: ${timing.domComplete - timing.domInteractive}`,
|
||||
);
|
||||
console.log(
|
||||
`从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`,
|
||||
);
|
||||
Sentry.captureMessage(
|
||||
`白屏时间 ${timing.responseStart - timing.navigationStart}`,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (['/login', '/initialization'].includes(props.location.pathname)) {
|
||||
document.title = `${
|
||||
(config.documentTitleMap as any)[props.location.pathname]
|
||||
} - 控制面板`;
|
||||
if (
|
||||
systemInfo?.isInitialized &&
|
||||
props.location.pathname === '/initialization'
|
||||
) {
|
||||
history.push('/crontab');
|
||||
}
|
||||
|
||||
if (systemInfo) {
|
||||
return React.Children.map(props.children, (child) => {
|
||||
return React.cloneElement(child, {
|
||||
...ctx,
|
||||
...theme,
|
||||
user,
|
||||
reloadUser,
|
||||
reloadTheme: setTheme,
|
||||
ws: ws.current,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
return (
|
||||
|
||||
const isFirefox = navigator.userAgent.includes('Firefox');
|
||||
const isSafari =
|
||||
navigator.userAgent.includes('Safari') &&
|
||||
!navigator.userAgent.includes('Chrome');
|
||||
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
|
||||
|
||||
const menu = (
|
||||
<Menu className="side-menu-user-drop-menu">
|
||||
<Menu.Item key="1" icon={<LogoutOutlined />} onClick={logout}>
|
||||
退出登录
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return loading ? (
|
||||
<PageLoading />
|
||||
) : (
|
||||
<ProLayout
|
||||
selectedKeys={[props.location.pathname]}
|
||||
title="控制面板"
|
||||
loading={loading}
|
||||
ErrorBoundary={Sentry.ErrorBoundary}
|
||||
title={
|
||||
<>
|
||||
<span style={{ fontSize: 16 }}>控制面板</span>
|
||||
<a
|
||||
href={changeLogLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: isFirefox ? 9 : 12,
|
||||
color: '#666',
|
||||
marginLeft: 2,
|
||||
zoom: isSafari ? 0.66 : 0.8,
|
||||
letterSpacing: isQQBrowser ? -2 : 0,
|
||||
}}
|
||||
>
|
||||
v{version}
|
||||
</span>
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
menuItemRender={(menuItemProps: any, defaultDom: any) => {
|
||||
if (
|
||||
menuItemProps.isUrl ||
|
||||
@@ -72,20 +267,58 @@ export default function (props: any) {
|
||||
}
|
||||
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
|
||||
}}
|
||||
postMenuData={(menuData) => {
|
||||
return [
|
||||
...(menuData || []),
|
||||
{
|
||||
icon: <LogoutOutlined />,
|
||||
name: '退出登录',
|
||||
path: 'logout',
|
||||
onTitleClick: () => logout(),
|
||||
},
|
||||
];
|
||||
pageTitleRender={(props, pageName, info) => {
|
||||
if (info && typeof info.pageName === 'string') {
|
||||
return `${info.pageName} - 控制面板`;
|
||||
}
|
||||
return '控制面板';
|
||||
}}
|
||||
onCollapse={setCollapsed}
|
||||
collapsed={collapsed}
|
||||
rightContentRender={() => (
|
||||
<Dropdown overlay={menu} trigger={['click']}>
|
||||
<span className="side-menu-user-wrapper">
|
||||
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
||||
<span style={{ marginLeft: 5 }}>admin</span>
|
||||
</span>
|
||||
</Dropdown>
|
||||
)}
|
||||
collapsedButtonRender={(collapsed) => (
|
||||
<span
|
||||
className="side-menu-container"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{!collapsed && !ctx.isPhone && (
|
||||
<Dropdown overlay={menu} trigger={['hover']}>
|
||||
<span className="side-menu-user-wrapper">
|
||||
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
||||
<span style={{ marginLeft: 5 }}>admin</span>
|
||||
</span>
|
||||
</Dropdown>
|
||||
)}
|
||||
<span
|
||||
className="side-menu-collapse-button"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{...defaultProps}
|
||||
>
|
||||
{props.children}
|
||||
{React.Children.map(props.children, (child) => {
|
||||
return React.cloneElement(child, {
|
||||
...ctx,
|
||||
...theme,
|
||||
user,
|
||||
reloadUser,
|
||||
reloadTheme: setTheme,
|
||||
socketMessage,
|
||||
});
|
||||
})}
|
||||
</ProLayout>
|
||||
);
|
||||
}
|
||||
|
||||
+93
-57
@@ -1,91 +1,127 @@
|
||||
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
|
||||
import { Button, notification, Modal } from 'antd';
|
||||
import React, {
|
||||
PureComponent,
|
||||
Fragment,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Button, message, Modal, TreeSelect } from 'antd';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||
import { request } from '@/utils/http';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||
|
||||
const Config = () => {
|
||||
const [width, setWdith] = useState('100%');
|
||||
const [marginLeft, setMarginLeft] = useState(0);
|
||||
const [marginTop, setMarginTop] = useState(-72);
|
||||
const Config = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [title, setTitle] = useState('config.sh');
|
||||
const [select, setSelect] = useState('config.sh');
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const editorRef = useRef<any>(null);
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
|
||||
const getConfig = () => {
|
||||
const getConfig = (name: string) => {
|
||||
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
|
||||
setValue(data.data);
|
||||
});
|
||||
};
|
||||
|
||||
const getFiles = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}config/config`)
|
||||
.then((data) => {
|
||||
setValue(data.data);
|
||||
.get(`${config.apiPrefix}configs/files`)
|
||||
.then((data: any) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const updateConfig = () => {
|
||||
setConfirmLoading(true);
|
||||
const content = editorRef.current
|
||||
? editorRef.current.getValue().replace(/\r\n/g, '\n')
|
||||
: value;
|
||||
|
||||
request
|
||||
.post(`${config.apiPrefix}save`, {
|
||||
data: { content: value, name: 'config.sh' },
|
||||
.post(`${config.apiPrefix}configs/save`, {
|
||||
data: { content, name: select },
|
||||
})
|
||||
.then((data) => {
|
||||
notification.success({
|
||||
message: data.msg,
|
||||
});
|
||||
.then((data: any) => {
|
||||
message.success(data.message);
|
||||
setConfirmLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onSelect = (value: any, node: any) => {
|
||||
setSelect(value);
|
||||
setTitle(node.value);
|
||||
getConfig(node.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (document.body.clientWidth < 768) {
|
||||
setWdith('auto');
|
||||
setMarginLeft(0);
|
||||
setMarginTop(0);
|
||||
} else {
|
||||
setWdith('100%');
|
||||
setMarginLeft(0);
|
||||
setMarginTop(-72);
|
||||
}
|
||||
getConfig();
|
||||
getFiles();
|
||||
getConfig('config.sh');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
className="code-mirror-wrapper"
|
||||
title="config.sh"
|
||||
className="ql-container-wrapper config-wrapper"
|
||||
title={title}
|
||||
loading={loading}
|
||||
extra={[
|
||||
<Button key="1" type="primary" onClick={updateConfig}>
|
||||
<TreeSelect
|
||||
className="config-select"
|
||||
value={select}
|
||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||
treeData={data}
|
||||
key="value"
|
||||
defaultValue="config.sh"
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
<Button
|
||||
key="1"
|
||||
loading={confirmLoading}
|
||||
type="primary"
|
||||
onClick={updateConfig}
|
||||
>
|
||||
保存
|
||||
</Button>,
|
||||
]}
|
||||
header={{
|
||||
style: {
|
||||
padding: '4px 16px 4px 15px',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 20,
|
||||
marginTop,
|
||||
width,
|
||||
marginLeft,
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
height: '100vh',
|
||||
style: headerStyle,
|
||||
}}
|
||||
>
|
||||
<CodeMirror
|
||||
value={value}
|
||||
options={{
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
styleActiveLine: true,
|
||||
matchBrackets: true,
|
||||
mode: 'shell',
|
||||
}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
setValue(value);
|
||||
}}
|
||||
onChange={(editor, data, value) => {}}
|
||||
/>
|
||||
{isPhone ? (
|
||||
<CodeMirror
|
||||
value={value}
|
||||
options={{
|
||||
lineNumbers: true,
|
||||
styleActiveLine: true,
|
||||
matchBrackets: true,
|
||||
mode: 'shell',
|
||||
}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
setValue(value);
|
||||
}}
|
||||
onChange={(editor, data, value) => {}}
|
||||
/>
|
||||
) : (
|
||||
<Editor
|
||||
defaultLanguage="shell"
|
||||
value={value}
|
||||
theme={theme}
|
||||
options={{
|
||||
fontSize: 12,
|
||||
lineNumbersMinChars: 3,
|
||||
folding: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
import React, { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
notification,
|
||||
Modal,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Typography,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SyncOutlined,
|
||||
CheckCircleOutlined,
|
||||
StopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { request } from '@/utils/http';
|
||||
import QRCode from 'qrcode.react';
|
||||
import CookieModal from './modal';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import './index.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
enum Status {
|
||||
'未获取',
|
||||
'正常',
|
||||
'已禁用',
|
||||
'已失效',
|
||||
'状态异常',
|
||||
}
|
||||
|
||||
enum StatusColor {
|
||||
'default',
|
||||
'success',
|
||||
'warning',
|
||||
'error',
|
||||
}
|
||||
|
||||
const type = 'DragableBodyRow';
|
||||
|
||||
const DragableBodyRow = ({
|
||||
index,
|
||||
moveRow,
|
||||
className,
|
||||
style,
|
||||
...restProps
|
||||
}: any) => {
|
||||
const ref = useRef();
|
||||
const [{ isOver, dropClassName }, drop] = useDrop(
|
||||
() => ({
|
||||
accept: type,
|
||||
collect: (monitor) => {
|
||||
const { index: dragIndex } = monitor.getItem() || ({} as any);
|
||||
if (dragIndex === index) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
isOver: monitor.isOver(),
|
||||
dropClassName:
|
||||
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
|
||||
};
|
||||
},
|
||||
drop: (item: any) => {
|
||||
moveRow(item.index, index);
|
||||
},
|
||||
}),
|
||||
[index],
|
||||
);
|
||||
const [, drag, preview] = useDrag(
|
||||
() => ({
|
||||
type,
|
||||
item: { index },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}),
|
||||
[index],
|
||||
);
|
||||
drop(drag(ref));
|
||||
|
||||
return (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={`${className}${isOver ? dropClassName : ''}`}
|
||||
style={{ cursor: 'move', ...style }}
|
||||
{...restProps}
|
||||
>
|
||||
{restProps.children}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const Config = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'pin',
|
||||
key: 'pin',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => {
|
||||
const match = record.value.match(/pt_pin=([^; ]+)(?=;?)/);
|
||||
const val = (match && match[1]) || '未匹配用户名';
|
||||
return <span style={{ cursor: 'text' }}>{decodeUrl(val)}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
align: 'center' as const,
|
||||
width: '15%',
|
||||
render: (text: string, record: any, index: number) => {
|
||||
return (
|
||||
<span style={{ cursor: 'text' }}>{record.nickname || '-'} </span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
align: 'center' as const,
|
||||
width: '50%',
|
||||
render: (text: string, record: any) => {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
display: 'inline-block',
|
||||
wordBreak: 'break-all',
|
||||
cursor: 'text',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
align: 'center' as const,
|
||||
width: '15%',
|
||||
render: (text: string, record: any, index: number) => {
|
||||
return (
|
||||
<Space size="middle" style={{ cursor: 'text' }}>
|
||||
<Tag
|
||||
color={StatusColor[record.status] || StatusColor[3]}
|
||||
style={{ marginRight: 0 }}
|
||||
>
|
||||
{Status[record.status]}
|
||||
</Tag>
|
||||
{record.status !== Status.已禁用 && (
|
||||
<Tooltip title="刷新">
|
||||
<a onClick={() => refreshStatus(record, index)}>
|
||||
<SyncOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any, index: number) => (
|
||||
<Space size="middle">
|
||||
<Tooltip title="编辑">
|
||||
<a onClick={() => editCookie(record, index)}>
|
||||
<EditOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip title={record.status === Status.已禁用 ? '启用' : '禁用'}>
|
||||
<a onClick={() => enabledOrDisabledCron(record, index)}>
|
||||
{record.status === Status.已禁用 ? (
|
||||
<CheckCircleOutlined />
|
||||
) : (
|
||||
<StopOutlined />
|
||||
)}
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<a onClick={() => deleteCookie(record, index)}>
|
||||
<DeleteOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
const [width, setWdith] = useState('100%');
|
||||
const [marginLeft, setMarginLeft] = useState(0);
|
||||
const [marginTop, setMarginTop] = useState(-72);
|
||||
const [value, setValue] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [editedCookie, setEditedCookie] = useState();
|
||||
|
||||
const getCookies = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}cookies`)
|
||||
.then((data: any) => {
|
||||
setValue(data.data);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const decodeUrl = (val: string) => {
|
||||
try {
|
||||
const newUrl = decodeURIComponent(val);
|
||||
return newUrl;
|
||||
} catch (error) {
|
||||
return val;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value && loading) {
|
||||
asyncUpdateStatus();
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const asyncUpdateStatus = async () => {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const cookie = value[i];
|
||||
if (cookie.status === Status.已禁用) {
|
||||
continue;
|
||||
}
|
||||
await sleep(1000);
|
||||
location.pathname === '/cookie' && refreshStatus(cookie, i);
|
||||
}
|
||||
};
|
||||
|
||||
const sleep = (time: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, time));
|
||||
};
|
||||
|
||||
const refreshStatus = (record: any, index: number) => {
|
||||
request
|
||||
.get(`${config.apiPrefix}cookies/${record._id}/refresh`)
|
||||
.then(async (data: any) => {
|
||||
if (data.data && data.data.value) {
|
||||
(value as any).splice(index, 1, data.data);
|
||||
setValue([...(value as any)] as any);
|
||||
} else {
|
||||
notification.error({ message: '更新状态失败' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const enabledOrDisabledCron = (record: any, index: number) => {
|
||||
Modal.confirm({
|
||||
title: `确认${record.status === Status.已禁用 ? '启用' : '禁用'}`,
|
||||
content: (
|
||||
<>
|
||||
确认{record.status === Status.已禁用 ? '启用' : '禁用'}
|
||||
Cookie{' '}
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{record.value}
|
||||
</Text>{' '}
|
||||
吗
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.get(
|
||||
`${config.apiPrefix}cookies/${record._id}/${
|
||||
record.status === Status.已禁用 ? 'enable' : 'disable'
|
||||
}`,
|
||||
{
|
||||
data: { _id: record._id },
|
||||
},
|
||||
)
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
notification.success({
|
||||
message: `${
|
||||
record.status === Status.已禁用 ? '启用' : '禁用'
|
||||
}成功`,
|
||||
});
|
||||
const newStatus =
|
||||
record.status === Status.已禁用 ? Status.未获取 : Status.已禁用;
|
||||
const result = [...value];
|
||||
result.splice(index, 1, {
|
||||
...record,
|
||||
status: newStatus,
|
||||
});
|
||||
setValue(result);
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const addCookie = () => {
|
||||
setEditedCookie(null as any);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const editCookie = (record: any, index: number) => {
|
||||
setEditedCookie(record);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const deleteCookie = (record: any, index: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: (
|
||||
<>
|
||||
确认删除Cookie{' '}
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{record.value}
|
||||
</Text>{' '}
|
||||
吗
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.delete(`${config.apiPrefix}cookies/${record._id}`)
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
notification.success({
|
||||
message: '删除成功',
|
||||
});
|
||||
const result = [...value];
|
||||
result.splice(index, 1);
|
||||
setValue(result);
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = (cookies: any[]) => {
|
||||
setIsModalVisible(false);
|
||||
if (cookies && cookies.length > 0) {
|
||||
handleCookies(cookies);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCookies = (cookies: any[]) => {
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i];
|
||||
const index = value.findIndex((x) => x._id === cookie._id);
|
||||
const result = [...value];
|
||||
if (index === -1) {
|
||||
result.push(cookie);
|
||||
} else {
|
||||
result.splice(index, 1, {
|
||||
...cookie,
|
||||
});
|
||||
}
|
||||
setValue(result);
|
||||
refreshStatus(cookie, index);
|
||||
}
|
||||
};
|
||||
|
||||
const components = {
|
||||
body: {
|
||||
row: DragableBodyRow,
|
||||
},
|
||||
};
|
||||
|
||||
const moveRow = useCallback(
|
||||
(dragIndex, hoverIndex) => {
|
||||
if (dragIndex === hoverIndex) {
|
||||
return;
|
||||
}
|
||||
const dragRow = value[dragIndex];
|
||||
const newData = [...value];
|
||||
newData.splice(dragIndex, 1);
|
||||
newData.splice(hoverIndex, 0, dragRow);
|
||||
setValue([...newData]);
|
||||
request
|
||||
.put(`${config.apiPrefix}cookies/${dragRow._id}/move`, {
|
||||
data: { fromIndex: dragIndex, toIndex: hoverIndex },
|
||||
})
|
||||
.then((data: any) => {
|
||||
if (data.code !== 200) {
|
||||
notification.error({
|
||||
message: data,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
[value],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (document.body.clientWidth < 768) {
|
||||
setWdith('auto');
|
||||
setMarginLeft(0);
|
||||
setMarginTop(0);
|
||||
} else {
|
||||
setWdith('100%');
|
||||
setMarginLeft(0);
|
||||
setMarginTop(-72);
|
||||
}
|
||||
getCookies();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
className="cookie-wrapper"
|
||||
title="Cookie管理"
|
||||
loading={loading}
|
||||
extra={[
|
||||
<Button key="2" type="primary" onClick={() => addCookie()}>
|
||||
添加Cookie
|
||||
</Button>,
|
||||
]}
|
||||
header={{
|
||||
style: {
|
||||
padding: '4px 16px 4px 15px',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 20,
|
||||
marginTop,
|
||||
width,
|
||||
marginLeft,
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Table
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
dataSource={value}
|
||||
rowKey="value"
|
||||
size="middle"
|
||||
bordered
|
||||
scroll={{ x: 768 }}
|
||||
components={components}
|
||||
onRow={(record, index) => {
|
||||
return {
|
||||
index,
|
||||
moveRow,
|
||||
} as any;
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
<CookieModal
|
||||
visible={isModalVisible}
|
||||
handleCancel={handleCancel}
|
||||
cookie={editedCookie}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Config;
|
||||
@@ -1,103 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, notification, Input, Form } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
|
||||
const CookieModal = ({
|
||||
cookie,
|
||||
handleCancel,
|
||||
visible,
|
||||
}: {
|
||||
cookie?: any;
|
||||
visible: boolean;
|
||||
handleCancel: (needUpdate?: boolean) => void;
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
const cookies = values.value
|
||||
.split('\n')
|
||||
.map((x: any) => x.trim().replace(/\s/g, ''));
|
||||
let flag = false;
|
||||
for (const coo of cookies) {
|
||||
if (!/pt_key=\S*;\s*pt_pin=\S*;\s*/.test(coo)) {
|
||||
notification.error({ message: `${coo}格式有误` });
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (flag) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const method = cookie ? 'put' : 'post';
|
||||
const payload = cookie ? { value: cookies[0], _id: cookie._id } : cookies;
|
||||
const { code, data } = await request[method](`${config.apiPrefix}cookies`, {
|
||||
data: payload,
|
||||
});
|
||||
if (code === 200) {
|
||||
notification.success({
|
||||
message: cookie ? '更新Cookie成功' : '添加Cookie成功',
|
||||
});
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
}, [cookie]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={cookie ? '编辑Cookie' : '新建Cookie'}
|
||||
visible={visible}
|
||||
forceRender
|
||||
onOk={() => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
handleOk(values);
|
||||
})
|
||||
.catch((info) => {
|
||||
console.log('Validate Failed:', info);
|
||||
});
|
||||
}}
|
||||
onCancel={() => handleCancel()}
|
||||
confirmLoading={loading}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="form_in_modal"
|
||||
preserve={false}
|
||||
initialValues={cookie}
|
||||
>
|
||||
<Form.Item
|
||||
name="value"
|
||||
rules={[
|
||||
{ required: true, message: '请输入Cookie' },
|
||||
{
|
||||
pattern: /pt_key=\S*;\s*pt_pin=\S*;\s*/,
|
||||
message: 'Cookie格式错误,注意分号(pt_key=***;pt_pin=***;)',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
autoSize={true}
|
||||
placeholder="请输入cookie,多个cookie换行输入"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookieModal;
|
||||
@@ -0,0 +1,3 @@
|
||||
.ant-table-pagination.ant-pagination {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
+530
-124
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
notification,
|
||||
message,
|
||||
Modal,
|
||||
Table,
|
||||
Tag,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from 'antd';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
SyncOutlined,
|
||||
Loading3QuartersOutlined,
|
||||
CloseCircleOutlined,
|
||||
FileTextOutlined,
|
||||
EllipsisOutlined,
|
||||
@@ -23,12 +23,19 @@ import {
|
||||
EditOutlined,
|
||||
StopOutlined,
|
||||
DeleteOutlined,
|
||||
PauseCircleOutlined,
|
||||
FieldTimeOutlined,
|
||||
PushpinOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { request } from '@/utils/http';
|
||||
import CronModal from './modal';
|
||||
import CronLogModal from './logModal';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { diffTime } from '@/utils/date';
|
||||
import { getTableScroll } from '@/utils/index';
|
||||
import './index.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Search } = Input;
|
||||
@@ -37,24 +44,59 @@ enum CrontabStatus {
|
||||
'running',
|
||||
'idle',
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
|
||||
const Crontab = () => {
|
||||
const columns = [
|
||||
const CrontabSort: any = { 0: 0, 3: 1, 1: 2, 4: 3 };
|
||||
|
||||
enum OperationName {
|
||||
'启用',
|
||||
'禁用',
|
||||
'运行',
|
||||
'停止',
|
||||
'置顶',
|
||||
'取消置顶',
|
||||
}
|
||||
|
||||
enum OperationPath {
|
||||
'enable',
|
||||
'disable',
|
||||
'run',
|
||||
'stop',
|
||||
'pin',
|
||||
'unpin',
|
||||
}
|
||||
|
||||
const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
const columns: any = [
|
||||
{
|
||||
title: '任务名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => (
|
||||
<span>{record.name || record._id}</span>
|
||||
<span>
|
||||
{record.name || record._id}{' '}
|
||||
{record.isPinned ? (
|
||||
<span>
|
||||
<PushpinOutlined />
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => a.name.localeCompare(b.name),
|
||||
multiple: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '任务',
|
||||
dataIndex: 'command',
|
||||
key: 'command',
|
||||
width: '40%',
|
||||
width: 250,
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => {
|
||||
return (
|
||||
@@ -70,31 +112,157 @@ const Crontab = () => {
|
||||
</span>
|
||||
);
|
||||
},
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => a.command.localeCompare(b.command),
|
||||
multiple: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '任务定时',
|
||||
dataIndex: 'schedule',
|
||||
key: 'schedule',
|
||||
width: 110,
|
||||
align: 'center' as const,
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
|
||||
multiple: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后运行时间',
|
||||
align: 'center' as const,
|
||||
width: 150,
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => {
|
||||
return a.last_execution_time - b.last_execution_time;
|
||||
},
|
||||
},
|
||||
render: (text: string, record: any) => {
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{record.last_execution_time
|
||||
? new Date(record.last_execution_time * 1000)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')
|
||||
: '-'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后运行时长',
|
||||
align: 'center' as const,
|
||||
width: 120,
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => {
|
||||
return a.last_running_time - b.last_running_time;
|
||||
},
|
||||
},
|
||||
render: (text: string, record: any) => {
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{record.last_running_time
|
||||
? diffTime(record.last_running_time)
|
||||
: '-'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '下次运行时间',
|
||||
align: 'center' as const,
|
||||
width: 150,
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => {
|
||||
return a.nextRunTime - b.nextRunTime;
|
||||
},
|
||||
},
|
||||
render: (text: string, record: any) => {
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{record.nextRunTime
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
align: 'center' as const,
|
||||
width: 85,
|
||||
filters: [
|
||||
{
|
||||
text: '运行中',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
text: '空闲中',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: '已禁用',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: '队列中',
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
onFilter: (value: number, record: any) => {
|
||||
if (record.isDisabled && record.status !== 0) {
|
||||
return value === 2;
|
||||
} else {
|
||||
return record.status === value;
|
||||
}
|
||||
},
|
||||
render: (text: string, record: any) => (
|
||||
<>
|
||||
{record.status === CrontabStatus.idle && (
|
||||
<Tag icon={<ClockCircleOutlined />} color="default">
|
||||
空闲中
|
||||
</Tag>
|
||||
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
|
||||
<>
|
||||
{record.status === CrontabStatus.idle && (
|
||||
<Tag icon={<ClockCircleOutlined />} color="default">
|
||||
空闲中
|
||||
</Tag>
|
||||
)}
|
||||
{record.status === CrontabStatus.running && (
|
||||
<Tag
|
||||
icon={<Loading3QuartersOutlined spin />}
|
||||
color="processing"
|
||||
>
|
||||
运行中
|
||||
</Tag>
|
||||
)}
|
||||
{record.status === CrontabStatus.queued && (
|
||||
<Tag icon={<FieldTimeOutlined />} color="default">
|
||||
队列中
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{record.status === CrontabStatus.running && (
|
||||
<Tag icon={<SyncOutlined spin />} color="processing">
|
||||
运行中
|
||||
</Tag>
|
||||
)}
|
||||
{record.status === CrontabStatus.disabled && (
|
||||
{record.isDisabled === 1 && record.status === CrontabStatus.idle && (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||
已禁用
|
||||
</Tag>
|
||||
@@ -106,35 +274,49 @@ const Crontab = () => {
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any, index: number) => (
|
||||
<Space size="middle">
|
||||
<Tooltip title="运行">
|
||||
<a
|
||||
onClick={() => {
|
||||
runCron(record, index);
|
||||
}}
|
||||
>
|
||||
<PlayCircleOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip title="日志">
|
||||
<a
|
||||
onClick={() => {
|
||||
setLogCron({ ...record, timestamp: Date.now() });
|
||||
}}
|
||||
>
|
||||
<FileTextOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<MoreBtn key="more" record={record} index={index} />
|
||||
</Space>
|
||||
),
|
||||
width: 100,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
const isPc = !isPhone;
|
||||
return (
|
||||
<Space size="middle">
|
||||
{record.status === CrontabStatus.idle && (
|
||||
<Tooltip title={isPc ? '运行' : ''}>
|
||||
<a
|
||||
onClick={() => {
|
||||
runCron(record, index);
|
||||
}}
|
||||
>
|
||||
<PlayCircleOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
{record.status !== CrontabStatus.idle && (
|
||||
<Tooltip title={isPc ? '停止' : ''}>
|
||||
<a
|
||||
onClick={() => {
|
||||
stopCron(record, index);
|
||||
}}
|
||||
>
|
||||
<PauseCircleOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={isPc ? '日志' : ''}>
|
||||
<a
|
||||
onClick={() => {
|
||||
setLogCron({ ...record, timestamp: Date.now() });
|
||||
}}
|
||||
>
|
||||
<FileTextOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<MoreBtn key="more" record={record} index={index} />
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const [width, setWdith] = useState('100%');
|
||||
const [marginLeft, setMarginLeft] = useState(0);
|
||||
const [marginTop, setMarginTop] = useState(-72);
|
||||
const [value, setValue] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
@@ -142,13 +324,39 @@ const Crontab = () => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||
const [logCron, setLogCron] = useState<any>();
|
||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||
|
||||
const getCrons = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}crons?searchValue=${searchText}`)
|
||||
.then((data: any) => {
|
||||
setValue(data.data.sort((a: any, b: any) => a.status - b.status));
|
||||
setValue(
|
||||
data.data
|
||||
.sort((a: any, b: any) => {
|
||||
const sortA = a.isDisabled ? 4 : a.status;
|
||||
const sortB = b.isDisabled ? 4 : b.status;
|
||||
a.isPinned = a.isPinned ? a.isPinned : 0;
|
||||
b.isPinned = b.isPinned ? b.isPinned : 0;
|
||||
if (a.isPinned === b.isPinned) {
|
||||
return CrontabSort[sortA] - CrontabSort[sortB];
|
||||
}
|
||||
return b.isPinned - a.isPinned;
|
||||
})
|
||||
.map((x) => {
|
||||
return {
|
||||
...x,
|
||||
nextRunTime: cron_parser
|
||||
.parseExpression(x.schedule)
|
||||
.next()
|
||||
.toDate(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
setCurrentPage(1);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
@@ -177,19 +385,16 @@ const Crontab = () => {
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.delete(`${config.apiPrefix}crons/${record._id}`)
|
||||
.delete(`${config.apiPrefix}crons`, { data: [record._id] })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
notification.success({
|
||||
message: '删除成功',
|
||||
});
|
||||
message.success('删除成功');
|
||||
const result = [...value];
|
||||
result.splice(index, 1);
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
result.splice(i, 1);
|
||||
setValue(result);
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
});
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -213,19 +418,54 @@ const Crontab = () => {
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.get(`${config.apiPrefix}crons/${record._id}/run`)
|
||||
.put(`${config.apiPrefix}crons/run`, { data: [record._id] })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
const result = [...value];
|
||||
result.splice(index, 1, {
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
result.splice(i, 1, {
|
||||
...record,
|
||||
status: CrontabStatus.running,
|
||||
});
|
||||
setValue(result);
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const stopCron = (record: any, index: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认停止',
|
||||
content: (
|
||||
<>
|
||||
确认停止定时任务{' '}
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{record.name}
|
||||
</Text>{' '}
|
||||
吗
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.put(`${config.apiPrefix}crons/stop`, { data: [record._id] })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
const result = [...value];
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
result.splice(i, 1, {
|
||||
...record,
|
||||
pid: null,
|
||||
status: CrontabStatus.idle,
|
||||
});
|
||||
setValue(result);
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -237,12 +477,10 @@ const Crontab = () => {
|
||||
|
||||
const enabledOrDisabledCron = (record: any, index: number) => {
|
||||
Modal.confirm({
|
||||
title: `确认${
|
||||
record.status === CrontabStatus.disabled ? '启用' : '禁用'
|
||||
}`,
|
||||
title: `确认${record.isDisabled === 1 ? '启用' : '禁用'}`,
|
||||
content: (
|
||||
<>
|
||||
确认{record.status === CrontabStatus.disabled ? '启用' : '禁用'}
|
||||
确认{record.isDisabled === 1 ? '启用' : '禁用'}
|
||||
定时任务{' '}
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{record.name}
|
||||
@@ -252,35 +490,70 @@ const Crontab = () => {
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.get(
|
||||
`${config.apiPrefix}crons/${record._id}/${
|
||||
record.status === CrontabStatus.disabled ? 'enable' : 'disable'
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${
|
||||
record.isDisabled === 1 ? 'enable' : 'disable'
|
||||
}`,
|
||||
{
|
||||
data: { _id: record._id },
|
||||
data: [record._id],
|
||||
},
|
||||
)
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
notification.success({
|
||||
message: `${
|
||||
record.status === CrontabStatus.disabled ? '启用' : '禁用'
|
||||
}成功`,
|
||||
});
|
||||
const newStatus =
|
||||
record.status === CrontabStatus.disabled
|
||||
? CrontabStatus.idle
|
||||
: CrontabStatus.disabled;
|
||||
const newStatus = record.isDisabled === 1 ? 0 : 1;
|
||||
const result = [...value];
|
||||
result.splice(index, 1, {
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
result.splice(i, 1, {
|
||||
...record,
|
||||
status: newStatus,
|
||||
isDisabled: newStatus,
|
||||
});
|
||||
setValue(result);
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const pinOrUnPinCron = (record: any, index: number) => {
|
||||
Modal.confirm({
|
||||
title: `确认${record.isPinned === 1 ? '取消置顶' : '置顶'}`,
|
||||
content: (
|
||||
<>
|
||||
确认{record.isPinned === 1 ? '取消置顶' : '置顶'}
|
||||
定时任务{' '}
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{record.name}
|
||||
</Text>{' '}
|
||||
吗
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${
|
||||
record.isPinned === 1 ? 'unpin' : 'pin'
|
||||
}`,
|
||||
{
|
||||
data: [record._id],
|
||||
},
|
||||
)
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
const newStatus = record.isPinned === 1 ? 0 : 1;
|
||||
const result = [...value];
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
result.splice(i, 1, {
|
||||
...record,
|
||||
isPinned: newStatus,
|
||||
});
|
||||
setValue(result);
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -296,31 +569,35 @@ const Crontab = () => {
|
||||
}> = ({ record, index }) => (
|
||||
<Dropdown
|
||||
arrow
|
||||
trigger={['click', 'hover']}
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<Menu onClick={({ key }) => action(key, record, index)}>
|
||||
<Menu.Item key="edit" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Menu.Item>
|
||||
{record.isSystem !== 1 && (
|
||||
<>
|
||||
<Menu.Item
|
||||
key="enableordisable"
|
||||
icon={
|
||||
record.status === CrontabStatus.disabled ? (
|
||||
<CheckCircleOutlined />
|
||||
) : (
|
||||
<StopOutlined />
|
||||
)
|
||||
}
|
||||
>
|
||||
{record.status === CrontabStatus.disabled ? '启用' : '禁用'}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
<Menu.Item
|
||||
key="enableOrDisable"
|
||||
icon={
|
||||
record.isDisabled === 1 ? (
|
||||
<CheckCircleOutlined />
|
||||
) : (
|
||||
<StopOutlined />
|
||||
)
|
||||
}
|
||||
>
|
||||
{record.isDisabled === 1 ? '启用' : '禁用'}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="pinOrUnPin"
|
||||
icon={
|
||||
record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />
|
||||
}
|
||||
>
|
||||
{record.isPinned === 1 ? '取消置顶' : '置顶'}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
@@ -335,12 +612,15 @@ const Crontab = () => {
|
||||
case 'edit':
|
||||
editCron(record, index);
|
||||
break;
|
||||
case 'enableordisable':
|
||||
case 'enableOrDisable':
|
||||
enabledOrDisabledCron(record, index);
|
||||
break;
|
||||
case 'delete':
|
||||
delCron(record, index);
|
||||
break;
|
||||
case 'pinOrUnPin':
|
||||
pinOrUnPinCron(record, index);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -354,14 +634,18 @@ const Crontab = () => {
|
||||
};
|
||||
|
||||
const onSearch = (value: string) => {
|
||||
setSearchText(value);
|
||||
setSearchText(value.trim());
|
||||
};
|
||||
|
||||
const handleCrons = (cron: any) => {
|
||||
const index = value.findIndex((x) => x._id === cron._id);
|
||||
const result = [...value];
|
||||
cron.nextRunTime = cron_parser
|
||||
.parseExpression(cron.schedule)
|
||||
.next()
|
||||
.toDate();
|
||||
if (index === -1) {
|
||||
result.push(cron);
|
||||
result.unshift(cron);
|
||||
} else {
|
||||
result.splice(index, 1, {
|
||||
...cron,
|
||||
@@ -376,6 +660,10 @@ const Crontab = () => {
|
||||
.then((data: any) => {
|
||||
const index = value.findIndex((x) => x._id === cron._id);
|
||||
const result = [...value];
|
||||
data.data.nextRunTime = cron_parser
|
||||
.parseExpression(data.data.schedule)
|
||||
.next()
|
||||
.toDate();
|
||||
result.splice(index, 1, {
|
||||
...cron,
|
||||
...data.data,
|
||||
@@ -385,8 +673,85 @@ const Crontab = () => {
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const onSelectChange = (selectedIds: any[]) => {
|
||||
setSelectedRowIds(selectedIds);
|
||||
|
||||
setTimeout(() => {
|
||||
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
|
||||
setTableScrollHeight(getTableScroll());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowIds,
|
||||
onChange: onSelectChange,
|
||||
selections: [
|
||||
Table.SELECTION_ALL,
|
||||
Table.SELECTION_INVERT,
|
||||
Table.SELECTION_NONE,
|
||||
],
|
||||
};
|
||||
|
||||
const delCrons = () => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: <>确认删除选中的定时任务吗</>,
|
||||
onOk() {
|
||||
request
|
||||
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
message.success('批量删除成功');
|
||||
setSelectedRowIds([]);
|
||||
getCrons();
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const operateCrons = (operationStatus: number) => {
|
||||
Modal.confirm({
|
||||
title: `确认${OperationName[operationStatus]}`,
|
||||
content: <>确认{OperationName[operationStatus]}选中的定时任务吗</>,
|
||||
onOk() {
|
||||
request
|
||||
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
|
||||
data: selectedRowIds,
|
||||
})
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
getCrons();
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onPageChange = (page: number, pageSize: number | undefined) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize as number);
|
||||
localStorage.setItem('pageSize', pageSize + '');
|
||||
};
|
||||
|
||||
const getRowClassName = (record: any, index: number) => {
|
||||
return record.isPinned ? 'pinned-cron' : '';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (logCron) {
|
||||
localStorage.setItem('logCron', logCron._id);
|
||||
setIsLogModalVisible(true);
|
||||
}
|
||||
}, [logCron]);
|
||||
@@ -396,22 +761,16 @@ const Crontab = () => {
|
||||
}, [searchText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (document.body.clientWidth < 768) {
|
||||
setWdith('auto');
|
||||
setMarginLeft(0);
|
||||
setMarginTop(0);
|
||||
} else {
|
||||
setWdith('100%');
|
||||
setMarginLeft(0);
|
||||
setMarginTop(-72);
|
||||
}
|
||||
setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
|
||||
setTimeout(() => {
|
||||
setTableScrollHeight(getTableScroll());
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
className="code-mirror-wrapper"
|
||||
className="ql-container-wrapper crontab-wrapper"
|
||||
title="定时任务"
|
||||
loading={loading}
|
||||
extra={[
|
||||
<Search
|
||||
placeholder="请输入名称或者关键词"
|
||||
@@ -421,34 +780,81 @@ const Crontab = () => {
|
||||
onSearch={onSearch}
|
||||
/>,
|
||||
<Button key="2" type="primary" onClick={() => addCron()}>
|
||||
添加定时
|
||||
添加任务
|
||||
</Button>,
|
||||
]}
|
||||
header={{
|
||||
style: {
|
||||
padding: '4px 16px 4px 15px',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 20,
|
||||
marginTop,
|
||||
width,
|
||||
marginLeft,
|
||||
},
|
||||
style: headerStyle,
|
||||
}}
|
||||
>
|
||||
{selectedRowIds.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
|
||||
批量删除
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => operateCrons(0)}
|
||||
style={{ marginLeft: 8, marginBottom: 5 }}
|
||||
>
|
||||
批量启用
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => operateCrons(1)}
|
||||
style={{ marginLeft: 8, marginRight: 8 }}
|
||||
>
|
||||
批量禁用
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => operateCrons(2)}
|
||||
>
|
||||
批量运行
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => operateCrons(3)}>
|
||||
批量停止
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => operateCrons(4)}
|
||||
style={{ marginLeft: 8, marginRight: 8 }}
|
||||
>
|
||||
批量置顶
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => operateCrons(5)}
|
||||
style={{ marginLeft: 8, marginRight: 8 }}
|
||||
>
|
||||
批量取消置顶
|
||||
</Button>
|
||||
<span style={{ marginLeft: 8 }}>
|
||||
已选择
|
||||
<a>{selectedRowIds?.length}</a>项
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Table
|
||||
columns={columns}
|
||||
pagination={{
|
||||
hideOnSinglePage: true,
|
||||
current: currentPage,
|
||||
onChange: onPageChange,
|
||||
pageSize: pageSize,
|
||||
showSizeChanger: true,
|
||||
simple: isPhone,
|
||||
defaultPageSize: 20,
|
||||
showTotal: (total: number, range: number[]) =>
|
||||
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
||||
}}
|
||||
dataSource={value}
|
||||
rowKey="_id"
|
||||
size="middle"
|
||||
bordered
|
||||
scroll={{ x: 768 }}
|
||||
scroll={{ x: 1000, y: tableScrollHeight }}
|
||||
loading={loading}
|
||||
rowSelection={rowSelection}
|
||||
rowClassName={getRowClassName}
|
||||
/>
|
||||
<CronLogModal
|
||||
visible={isLogModalVisible}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, notification, Input, Form } from 'antd';
|
||||
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import {
|
||||
Loading3QuartersOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { PageLoading } from '@ant-design/pro-layout';
|
||||
|
||||
enum CrontabStatus {
|
||||
'running',
|
||||
'idle',
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
const { Countdown } = Statistic;
|
||||
|
||||
const CronLogModal = ({
|
||||
cron,
|
||||
@@ -20,6 +27,9 @@ const CronLogModal = ({
|
||||
}) => {
|
||||
const [value, setValue] = useState<string>('启动中...');
|
||||
const [loading, setLoading] = useState<any>(true);
|
||||
const [executing, setExecuting] = useState<any>(true);
|
||||
const [isPhone, setIsPhone] = useState(false);
|
||||
const [theme, setTheme] = useState<string>('');
|
||||
|
||||
const getCronLog = (isFirst?: boolean) => {
|
||||
if (isFirst) {
|
||||
@@ -28,12 +38,40 @@ const CronLogModal = ({
|
||||
request
|
||||
.get(`${config.apiPrefix}crons/${cron._id}/log`)
|
||||
.then((data: any) => {
|
||||
const log = data.data as string;
|
||||
setValue(log || '暂无日志');
|
||||
if (log && !log.includes('执行结束')) {
|
||||
setTimeout(() => {
|
||||
getCronLog();
|
||||
}, 2000);
|
||||
if (localStorage.getItem('logCron') === cron._id) {
|
||||
const log = data.data as string;
|
||||
setValue(log || '暂无日志');
|
||||
setExecuting(
|
||||
log && !log.includes('执行结束') && !log.includes('重启面板'),
|
||||
);
|
||||
if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
|
||||
setTimeout(() => {
|
||||
getCronLog();
|
||||
}, 2000);
|
||||
}
|
||||
if (
|
||||
log &&
|
||||
log.includes('重启面板') &&
|
||||
cron.status === CrontabStatus.running
|
||||
) {
|
||||
message.warning({
|
||||
content: (
|
||||
<span>
|
||||
系统将在
|
||||
<Countdown
|
||||
className="inline-countdown"
|
||||
format="ss"
|
||||
value={Date.now() + 1000 * 10}
|
||||
/>
|
||||
秒后自动刷新
|
||||
</span>
|
||||
),
|
||||
duration: 10,
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -44,26 +82,67 @@ const CronLogModal = ({
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
localStorage.removeItem('logCron');
|
||||
handleCancel();
|
||||
};
|
||||
|
||||
const titleElement = () => {
|
||||
return (
|
||||
<>
|
||||
{(executing || loading) && <Loading3QuartersOutlined spin />}
|
||||
{!executing && <CheckCircleOutlined />}
|
||||
<span style={{ marginLeft: 5 }}>日志-{cron && cron.name}</span>{' '}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (cron) {
|
||||
getCronLog(true);
|
||||
}
|
||||
}, [cron]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsPhone(document.body.clientWidth < 768);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`日志-${cron && cron.name}`}
|
||||
title={titleElement()}
|
||||
visible={visible}
|
||||
centered
|
||||
className="log-modal"
|
||||
bodyStyle={{
|
||||
overflowY: 'auto',
|
||||
maxHeight: 'calc(80vh - var(--vh-offset, 0px))',
|
||||
minHeight: '300px',
|
||||
}}
|
||||
forceRender
|
||||
onOk={() => cancel()}
|
||||
onCancel={() => cancel()}
|
||||
footer={[
|
||||
<Button type="primary" onClick={() => cancel()}>
|
||||
知道了
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
|
||||
{!loading && value}
|
||||
</pre>
|
||||
{loading ? (
|
||||
<PageLoading />
|
||||
) : (
|
||||
<pre
|
||||
style={
|
||||
isPhone
|
||||
? {
|
||||
fontFamily: 'Source Code Pro',
|
||||
width: 375,
|
||||
zoom: 0.83,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</pre>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+25
-21
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, notification, Input, Form } from 'antd';
|
||||
import { Modal, message, Input, Form } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import cronParse from 'cron-parser';
|
||||
@@ -27,25 +27,17 @@ const CronModal = ({
|
||||
data: payload,
|
||||
});
|
||||
if (code === 200) {
|
||||
notification.success({
|
||||
message: cron ? '更新Cron成功' : '添加Cron成功',
|
||||
});
|
||||
message.success(cron ? '更新Cron成功' : '添加Cron成功');
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
});
|
||||
message.error(data);
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (cron) {
|
||||
form.setFieldsValue(cron);
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [cron]);
|
||||
form.resetFields();
|
||||
}, [cron, visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -64,23 +56,35 @@ const CronModal = ({
|
||||
}}
|
||||
onCancel={() => handleCancel()}
|
||||
confirmLoading={loading}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" name="form_in_modal" preserve={false}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="form_in_modal"
|
||||
initialValues={cron}
|
||||
>
|
||||
<Form.Item name="name" label="名称">
|
||||
<Input />
|
||||
<Input placeholder="请输入任务名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="command" label="任务" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
<Form.Item
|
||||
name="command"
|
||||
label="命令"
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
autoSize={true}
|
||||
placeholder="请输入要执行的命令"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="schedule"
|
||||
label="时间"
|
||||
label="定时规则"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (cronParse.parseString(value).expressions.length > 0) {
|
||||
if (cronParse.parseExpression(value).hasNext()) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject('Cron表达式格式有误');
|
||||
@@ -89,7 +93,7 @@ const CronModal = ({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
<Input placeholder="秒(可选) 分 时 天 月 周" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
import React, { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
message,
|
||||
Modal,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Input,
|
||||
Tabs,
|
||||
} from 'antd';
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SyncOutlined,
|
||||
CheckCircleOutlined,
|
||||
StopOutlined,
|
||||
BugOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { request } from '@/utils/http';
|
||||
import DependenceModal from './modal';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import './index.less';
|
||||
import { getTableScroll } from '@/utils/index';
|
||||
import DependenceLogModal from './logModal';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Search } = Input;
|
||||
|
||||
enum Status {
|
||||
'安装中',
|
||||
'已安装',
|
||||
'安装失败',
|
||||
'删除中',
|
||||
'已删除',
|
||||
'删除失败',
|
||||
}
|
||||
|
||||
enum StatusColor {
|
||||
'processing',
|
||||
'success',
|
||||
'error',
|
||||
}
|
||||
|
||||
const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
const columns: any = [
|
||||
{
|
||||
title: '序号',
|
||||
align: 'center' as const,
|
||||
width: 50,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
return (
|
||||
<Space size="middle" style={{ cursor: 'text' }}>
|
||||
<Tag
|
||||
color={StatusColor[record.status % 3]}
|
||||
style={{ marginRight: 0 }}
|
||||
>
|
||||
{Status[record.status]}
|
||||
</Tag>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created',
|
||||
dataIndex: 'created',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => {
|
||||
return <span>{new Date(record.created).toLocaleString()}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
const isPc = !isPhone;
|
||||
return (
|
||||
<Space size="middle">
|
||||
{record.status !== Status.安装中 &&
|
||||
record.status !== Status.删除中 && (
|
||||
<>
|
||||
<Tooltip title={isPc ? '重新安装' : ''}>
|
||||
<a onClick={() => reInstallDependence(record, index)}>
|
||||
<BugOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip title={isPc ? '删除' : ''}>
|
||||
<a onClick={() => deleteDependence(record, index)}>
|
||||
<DeleteOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<Tooltip title={isPc ? '日志' : ''}>
|
||||
<a
|
||||
onClick={() => {
|
||||
setLogDependence({ ...record, timestamp: Date.now() });
|
||||
}}
|
||||
>
|
||||
<FileTextOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const [value, setValue] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [editedDependence, setEditedDependence] = useState();
|
||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||
const [logDependence, setLogDependence] = useState<any>();
|
||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||
const [type, setType] = useState('nodejs');
|
||||
|
||||
const getDependencies = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
.get(
|
||||
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
|
||||
)
|
||||
.then((data: any) => {
|
||||
setValue(data.data);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const addDependence = () => {
|
||||
setEditedDependence(null as any);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const editDependence = (record: any, index: number) => {
|
||||
setEditedDependence(record);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const deleteDependence = (record: any, index: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: (
|
||||
<>
|
||||
确认删除依赖{' '}
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{record.name}
|
||||
</Text>{' '}
|
||||
吗
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.delete(`${config.apiPrefix}dependencies`, { data: [record._id] })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
handleDependence(data.data[0]);
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const reInstallDependence = (record: any, index: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认重新安装',
|
||||
content: (
|
||||
<>
|
||||
确认重新安装{' '}
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{record.name}
|
||||
</Text>{' '}
|
||||
吗
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.put(`${config.apiPrefix}dependencies/reinstall`, {
|
||||
data: [record._id],
|
||||
})
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
handleDependence(data.data[0]);
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = (dependence?: any[]) => {
|
||||
setIsModalVisible(false);
|
||||
dependence && handleDependence(dependence);
|
||||
};
|
||||
|
||||
const handleDependence = (dependence: any) => {
|
||||
const result = [...value];
|
||||
if (Array.isArray(dependence)) {
|
||||
result.push(...dependence);
|
||||
} else {
|
||||
const index = value.findIndex((x) => x._id === dependence._id);
|
||||
result.splice(index, 1, {
|
||||
...dependence,
|
||||
});
|
||||
}
|
||||
setValue(result);
|
||||
};
|
||||
|
||||
const onSelectChange = (selectedIds: any[]) => {
|
||||
setSelectedRowIds(selectedIds);
|
||||
|
||||
setTimeout(() => {
|
||||
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
|
||||
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowIds,
|
||||
onChange: onSelectChange,
|
||||
};
|
||||
|
||||
const delDependencies = () => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: <>确认删除选中的依赖吗</>,
|
||||
onOk() {
|
||||
request
|
||||
.delete(`${config.apiPrefix}dependencies`, { data: selectedRowIds })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
setSelectedRowIds([]);
|
||||
getDependencies();
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getDependenceDetail = (dependence: any) => {
|
||||
request
|
||||
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
|
||||
.then((data: any) => {
|
||||
const index = value.findIndex((x) => x._id === dependence._id);
|
||||
const result = [...value];
|
||||
result.splice(index, 1, {
|
||||
...dependence,
|
||||
...data.data,
|
||||
});
|
||||
setValue(result);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const onSearch = (value: string) => {
|
||||
setSearchText(value.trim());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDependencies();
|
||||
}, [searchText, type]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (logDependence) {
|
||||
localStorage.setItem('logDependence', logDependence._id);
|
||||
setIsLogModalVisible(true);
|
||||
}
|
||||
}, [logDependence]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socketMessage) return;
|
||||
const { type, message, references } = socketMessage;
|
||||
if (
|
||||
type === 'installDependence' &&
|
||||
message.includes('结束时间') &&
|
||||
references.length > 0
|
||||
) {
|
||||
let status;
|
||||
if (message.includes('安装')) {
|
||||
status = message.includes('成功') ? Status.已安装 : Status.安装失败;
|
||||
} else {
|
||||
status = message.includes('成功') ? Status.已删除 : Status.删除失败;
|
||||
}
|
||||
const result = [...value];
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
const index = value.findIndex((x) => x._id === references[i]);
|
||||
result.splice(index, 1, {
|
||||
...result[index],
|
||||
status,
|
||||
});
|
||||
}
|
||||
setValue(result);
|
||||
|
||||
if (status === Status.已删除) {
|
||||
setTimeout(() => {
|
||||
const _result = [...value];
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
const index = value.findIndex((x) => x._id === references[i]);
|
||||
_result.splice(index, 1);
|
||||
}
|
||||
setValue(_result);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}, [socketMessage]);
|
||||
|
||||
const panelContent = () => (
|
||||
<>
|
||||
{selectedRowIds.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ marginBottom: 5, marginLeft: 8 }}
|
||||
onClick={delDependencies}
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
<span style={{ marginLeft: 8 }}>
|
||||
已选择
|
||||
<a>{selectedRowIds?.length}</a>项
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Table
|
||||
columns={columns}
|
||||
rowSelection={rowSelection}
|
||||
pagination={false}
|
||||
dataSource={value}
|
||||
rowKey="_id"
|
||||
size="middle"
|
||||
scroll={{ x: 768, y: tableScrollHeight }}
|
||||
loading={loading}
|
||||
/>
|
||||
</DndProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
const onTabChange = (activeKey: string) => {
|
||||
setType(activeKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
className="ql-container-wrapper dependence-wrapper"
|
||||
title="依赖管理"
|
||||
extra={[
|
||||
<Search
|
||||
placeholder="请输入名称"
|
||||
style={{ width: 'auto' }}
|
||||
enterButton
|
||||
loading={loading}
|
||||
onSearch={onSearch}
|
||||
/>,
|
||||
<Button key="2" type="primary" onClick={() => addDependence()}>
|
||||
添加依赖
|
||||
</Button>,
|
||||
]}
|
||||
header={{
|
||||
style: headerStyle,
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
defaultActiveKey="nodejs"
|
||||
size="small"
|
||||
tabPosition="top"
|
||||
onChange={onTabChange}
|
||||
>
|
||||
<Tabs.TabPane tab="NodeJs" key="nodejs">
|
||||
{panelContent()}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Python3" key="python3">
|
||||
{panelContent()}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Linux" key="linux">
|
||||
{panelContent()}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<DependenceModal
|
||||
visible={isModalVisible}
|
||||
handleCancel={handleCancel}
|
||||
dependence={editedDependence}
|
||||
defaultType={type}
|
||||
/>
|
||||
<DependenceLogModal
|
||||
visible={isLogModalVisible}
|
||||
handleCancel={(needRemove?: boolean) => {
|
||||
setIsLogModalVisible(false);
|
||||
if (needRemove) {
|
||||
const index = value.findIndex((x) => x._id === logDependence._id);
|
||||
const result = [...value];
|
||||
result.splice(index, 1);
|
||||
setValue(result);
|
||||
} else if ([...value].map((x) => x._id).includes(logDependence._id)) {
|
||||
getDependenceDetail(logDependence);
|
||||
}
|
||||
}}
|
||||
socketMessage={socketMessage}
|
||||
dependence={logDependence}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dependence;
|
||||
@@ -0,0 +1,150 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import {
|
||||
Loading3QuartersOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { PageLoading } from '@ant-design/pro-layout';
|
||||
|
||||
const DependenceLogModal = ({
|
||||
dependence,
|
||||
handleCancel,
|
||||
visible,
|
||||
socketMessage,
|
||||
}: {
|
||||
dependence?: any;
|
||||
visible: boolean;
|
||||
handleCancel: (needRemove?: boolean) => void;
|
||||
socketMessage: any;
|
||||
}) => {
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [executing, setExecuting] = useState<any>(true);
|
||||
const [isPhone, setIsPhone] = useState(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [isRemoveFailed, setIsRemoveFailed] = useState(false);
|
||||
const [removeLoading, setRemoveLoading] = useState<boolean>(false);
|
||||
|
||||
const cancel = (needRemove: boolean = false) => {
|
||||
localStorage.removeItem('logDependence');
|
||||
handleCancel(needRemove);
|
||||
};
|
||||
|
||||
const titleElement = () => {
|
||||
return (
|
||||
<>
|
||||
{executing && <Loading3QuartersOutlined spin />}
|
||||
{!executing && <CheckCircleOutlined />}
|
||||
<span style={{ marginLeft: 5 }}>
|
||||
日志 - {dependence && dependence.name}
|
||||
</span>{' '}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getDependenceLog = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
|
||||
.then((data: any) => {
|
||||
if (localStorage.getItem('logDependence') === dependence._id) {
|
||||
const log = (data.data.log || []).join('\n') as string;
|
||||
setValue(log);
|
||||
setExecuting(!log.includes('结束时间'));
|
||||
setIsRemoveFailed(log.includes('删除失败'));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const forceRemoveDependence = () => {
|
||||
setRemoveLoading(true);
|
||||
request
|
||||
.delete(`${config.apiPrefix}dependencies/force`, {
|
||||
data: [dependence._id],
|
||||
})
|
||||
.then((data: any) => {
|
||||
cancel(true);
|
||||
})
|
||||
.finally(() => {
|
||||
setRemoveLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const footerClick = () => {
|
||||
if (isRemoveFailed) {
|
||||
forceRemoveDependence();
|
||||
} else {
|
||||
cancel();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dependence) {
|
||||
getDependenceLog();
|
||||
}
|
||||
}, [dependence]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socketMessage) return;
|
||||
const { type, message, references } = socketMessage;
|
||||
if (
|
||||
type === 'installDependence' &&
|
||||
message.includes('结束时间') &&
|
||||
references.length > 0
|
||||
) {
|
||||
setExecuting(false);
|
||||
setIsRemoveFailed(message.includes('删除失败'));
|
||||
}
|
||||
setValue(`${value} \n ${message}`);
|
||||
}, [socketMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsPhone(document.body.clientWidth < 768);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={titleElement()}
|
||||
visible={visible}
|
||||
centered
|
||||
className="log-modal"
|
||||
bodyStyle={{
|
||||
overflowY: 'auto',
|
||||
maxHeight: 'calc(70vh - var(--vh-offset, 0px))',
|
||||
minHeight: '300px',
|
||||
}}
|
||||
forceRender
|
||||
onOk={() => cancel()}
|
||||
onCancel={() => cancel()}
|
||||
footer={[
|
||||
<Button type="primary" onClick={footerClick} loading={removeLoading}>
|
||||
{isRemoveFailed ? '强制删除' : '知道了'}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{loading ? (
|
||||
<PageLoading />
|
||||
) : (
|
||||
<pre
|
||||
style={
|
||||
isPhone
|
||||
? {
|
||||
fontFamily: 'Source Code Pro',
|
||||
width: 375,
|
||||
zoom: 0.83,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</pre>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DependenceLogModal;
|
||||
@@ -0,0 +1,137 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, message, Input, Form, Radio, Select } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
|
||||
const { Option } = Select;
|
||||
enum DependenceTypes {
|
||||
'nodejs',
|
||||
'python3',
|
||||
'linux',
|
||||
}
|
||||
|
||||
const DependenceModal = ({
|
||||
dependence,
|
||||
handleCancel,
|
||||
visible,
|
||||
defaultType,
|
||||
}: {
|
||||
dependence?: any;
|
||||
visible: boolean;
|
||||
handleCancel: (cks?: any[]) => void;
|
||||
defaultType: string;
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
setLoading(true);
|
||||
const { name, split, type } = values;
|
||||
const method = dependence ? 'put' : 'post';
|
||||
let payload;
|
||||
if (!dependence) {
|
||||
if (split === '1') {
|
||||
const symbol = name.includes('&') ? '&' : '\n';
|
||||
payload = name.split(symbol).map((x: any) => {
|
||||
return {
|
||||
name: x,
|
||||
type,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
payload = [{ name, type }];
|
||||
}
|
||||
} else {
|
||||
payload = { ...values, _id: dependence._id };
|
||||
}
|
||||
try {
|
||||
const { code, data } = await request[method](
|
||||
`${config.apiPrefix}dependencies`,
|
||||
{
|
||||
data: payload,
|
||||
},
|
||||
);
|
||||
if (code !== 200) {
|
||||
message.error(data);
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
}, [dependence, visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={dependence ? '编辑依赖' : '新建依赖'}
|
||||
visible={visible}
|
||||
forceRender
|
||||
onOk={() => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
handleOk(values);
|
||||
})
|
||||
.catch((info) => {
|
||||
console.log('Validate Failed:', info);
|
||||
});
|
||||
}}
|
||||
onCancel={() => handleCancel()}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="dependence_modal"
|
||||
initialValues={dependence}
|
||||
>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="依赖类型"
|
||||
initialValue={DependenceTypes[defaultType as any]}
|
||||
>
|
||||
<Select>
|
||||
{config.dependenceTypes.map((x, i) => (
|
||||
<Option value={i}>{x}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{!dependence && (
|
||||
<Form.Item
|
||||
name="split"
|
||||
label="自动拆分"
|
||||
initialValue="0"
|
||||
tooltip="多个依赖是否换行分割"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="1">是</Radio>
|
||||
<Radio value="0">否</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入依赖名称', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
autoSize={true}
|
||||
placeholder="请输入依赖名称"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input placeholder="请输入备注" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DependenceModal;
|
||||
@@ -1,12 +1,19 @@
|
||||
.d2h-files-diff {
|
||||
height: calc(100vh - 130px);
|
||||
overflow: auto;
|
||||
height: calc(100vh - 130px);
|
||||
height: calc(100vh - var(--vh-offset, 0px) - 130px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.d2h-code-side-linenumber {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.d2h-code-side-line {
|
||||
padding: 0 0.5em;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.diff-switch-file {
|
||||
.ant-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user