mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
742d0b2135 | ||
|
|
2081364325 | ||
|
|
39dc31916e | ||
|
|
5395cebdb4 | ||
|
|
fe934fa4af | ||
|
|
461fa8e131 | ||
|
|
4618a19c04 | ||
|
|
e2bd15683e | ||
|
|
648b9c4520 | ||
|
|
b4e4e84bbd | ||
|
|
aa5d6f3cb6 | ||
|
|
e9416c23df | ||
|
|
ef4999be55 | ||
|
|
e7be4999b0 | ||
|
|
5907553670 | ||
|
|
0b9066525a | ||
|
|
ac904cae61 | ||
|
|
4d5fa320ea | ||
|
|
003defedcf | ||
|
|
40e8041401 | ||
|
|
a8174e89be | ||
|
|
cce83f7a4c | ||
|
|
4f7649f157 | ||
|
|
b002cbef3a | ||
|
|
1d93fe0de0 | ||
|
|
2d936f1341 | ||
|
|
b40673e9ac | ||
|
|
637874e426 | ||
|
|
de71d1a258 | ||
|
|
e174e190ee | ||
|
|
89164a3bd2 | ||
|
|
a9b9c4d7ad | ||
|
|
f947866c32 | ||
|
|
a2e33d1ed4 | ||
|
|
b1a242c8ee | ||
|
|
0bf28e9ab3 | ||
|
|
624c9374e5 | ||
|
|
a9da8fffc1 | ||
|
|
b4e5db9da9 | ||
|
|
2e27d4057e | ||
|
|
e7d023a7e0 | ||
|
|
39bfd39559 | ||
|
|
6832cb4e22 | ||
|
|
590dbba19b | ||
|
|
3987ef8d97 | ||
|
|
8b0dedaf8c | ||
|
|
04edd97967 | ||
|
|
6131fe0110 | ||
|
|
665c344ae4 | ||
|
|
373b8c97d7 | ||
|
|
56eb0c5408 | ||
|
|
fe55929959 | ||
|
|
01e2bd007d | ||
|
|
4e091b0c3e | ||
|
|
db94cd3799 | ||
|
|
a15192b9b2 | ||
|
|
efd4f1d5ab | ||
|
|
bd166ee794 | ||
|
|
93e94ea94c | ||
|
|
88b87de391 | ||
|
|
8affff96f3 | ||
|
|
936b565fb1 | ||
|
|
b69ff2895e | ||
|
|
f3791cbb62 | ||
|
|
b0f3b51736 | ||
|
|
3aa112e373 | ||
|
|
683482e067 | ||
|
|
5a2caeb66b | ||
|
|
ee6e5bd8b4 | ||
|
|
f970322f0a | ||
|
|
1718120623 | ||
|
|
702c3160ec | ||
|
|
db227e56bf | ||
|
|
84828865f7 | ||
|
|
87455cebc0 | ||
|
|
9f4435b237 | ||
|
|
e5bc4b3b04 | ||
|
|
11e1b50000 | ||
|
|
bec226ae13 | ||
|
|
c9ecdc6c97 | ||
|
|
4cf64e7af0 | ||
|
|
4ce01c5f0e | ||
|
|
2eff99abe1 | ||
|
|
5bf57185d9 | ||
|
|
c59dac19ae | ||
|
|
f1ef8e72ae | ||
|
|
5b1accffb7 | ||
|
|
f64ff83414 | ||
|
|
26c914d1db | ||
|
|
8f75a54110 | ||
|
|
874b6e3a56 | ||
|
|
f305d45225 | ||
|
|
a7f97bd729 | ||
|
|
0bbb7c7b89 | ||
|
|
f7465759c7 | ||
|
|
998e82d323 | ||
|
|
f8dfee8945 | ||
|
|
86e3d8736b | ||
|
|
85d32d00e1 | ||
|
|
6a52e2f804 | ||
|
|
b4cb72c5d0 | ||
|
|
79fd4ec58f | ||
|
|
49ed5c6632 | ||
|
|
ffba7ee4a1 | ||
|
|
91d080472f | ||
|
|
826f214f0f | ||
|
|
ca150dc6a6 | ||
|
|
ae5db60211 | ||
|
|
490bdc15f6 | ||
|
|
1377fe31aa | ||
|
|
3d1cf6bf2e | ||
|
|
6ac755b478 | ||
|
|
e70fa83074 | ||
|
|
41521f1d08 | ||
|
|
8b7bef097c | ||
|
|
618756d4a9 | ||
|
|
70d0c71414 | ||
|
|
65eb9c806d | ||
|
|
50718ba714 | ||
|
|
7c3bb90d12 | ||
|
|
f66390eabd | ||
|
|
481646501c | ||
|
|
5baa8e4391 | ||
|
|
3d11210aeb | ||
|
|
e1863407f2 | ||
|
|
0c28b63ab7 | ||
|
|
8b8bd279c6 | ||
|
|
01b404765e | ||
|
|
c231a871d7 | ||
|
|
598c6b4e57 | ||
|
|
2295fabcc5 | ||
|
|
be94d796ac | ||
|
|
c84908d7fa | ||
|
|
d8ae039b92 | ||
|
|
bce1431d03 | ||
|
|
9db0095e29 | ||
|
|
77dc7817fb | ||
|
|
d0dd97631e | ||
|
|
d11d6d0c18 | ||
|
|
0af687f781 | ||
|
|
8db997abe8 | ||
|
|
aab6bbeb15 | ||
|
|
df1addc1ff | ||
|
|
1f8f35476a | ||
|
|
5354fc76db | ||
|
|
b29a9e012c | ||
|
|
6e5d89c197 | ||
|
|
d6cfb18f06 | ||
|
|
9a3e38051d | ||
|
|
b705ad6ee8 | ||
|
|
db564d1add | ||
|
|
4f422c0658 | ||
|
|
f267720af4 | ||
|
|
4a0c66bcc0 | ||
|
|
ef7283a9fd | ||
|
|
4498449eea | ||
|
|
ebb9676a51 | ||
|
|
6fb39ce835 | ||
|
|
59c26d90d3 | ||
|
|
1d2df860e8 | ||
|
|
ce22cad5b2 | ||
|
|
dc44ce6b1a | ||
|
|
b27ee23cc3 | ||
|
|
083c8869aa | ||
|
|
6a971a0d6e | ||
|
|
a25bfb6912 | ||
|
|
f19dd21155 | ||
|
|
79b342d1d2 | ||
|
|
20b0d75b0d | ||
|
|
c89724906e | ||
|
|
c3a548ece3 | ||
|
|
b431e39f8f | ||
|
|
710746809b | ||
|
|
8a225e9beb | ||
|
|
a5ecb204b2 | ||
|
|
f1f009da3b | ||
|
|
7291098f98 | ||
|
|
666545ff03 | ||
|
|
81e2bbb14c | ||
|
|
970dff144c | ||
|
|
819b15b15e | ||
|
|
3fa57c69ac | ||
|
|
18689c8119 | ||
|
|
61e7049baa | ||
|
|
39c40a328d | ||
|
|
955d815d14 | ||
|
|
3a3b945de8 | ||
|
|
73c53dbc8d | ||
|
|
f76828d4ec | ||
|
|
7b2956eb0a | ||
|
|
b2167ae5e4 | ||
|
|
98dfa4c425 | ||
|
|
5bf08b76de | ||
|
|
5c5b4f804e | ||
|
|
6455245d11 | ||
|
|
2ff3d9c601 | ||
|
|
d342fbf29f | ||
|
|
f865018cd6 | ||
|
|
3b9a4f0834 | ||
|
|
67bc305950 | ||
|
|
97f128d59f | ||
|
|
e25885c4d9 | ||
|
|
c285026339 | ||
|
|
6c34045a48 | ||
|
|
e1e6261b4f | ||
|
|
697bcb5922 | ||
|
|
e05b0a2491 | ||
|
|
e1655455d8 | ||
|
|
9921dab064 | ||
|
|
403ab7f0f0 | ||
|
|
9bc7ca4094 | ||
|
|
e924aa76b0 | ||
|
|
0c46606e14 | ||
|
|
dbfa4049f5 | ||
|
|
622fe2a8f8 | ||
|
|
7bce5c4f6a | ||
|
|
1f7f2c8971 | ||
|
|
437a849c2a | ||
|
|
8f7261dd50 | ||
|
|
76fa82c3a7 | ||
|
|
46c9774efa | ||
|
|
1bc64a23e3 | ||
|
|
cc9a17afa3 | ||
|
|
fcbcfc3a03 | ||
|
|
0144f3b131 | ||
|
|
518436d5ca | ||
|
|
1961a6b913 | ||
|
|
5edebdd5f3 | ||
|
|
48edf2c212 | ||
|
|
9b16dca9da | ||
|
|
9479c2498c | ||
|
|
4a18bcabe2 | ||
|
|
34f690c53d | ||
|
|
e70cd6579f | ||
|
|
99c047e9c7 | ||
|
|
978ae326f8 | ||
|
|
b01f27f2d3 | ||
|
|
4d34e0dd96 | ||
|
|
9b0065e533 | ||
|
|
34a51feee9 | ||
|
|
679091de59 | ||
|
|
c6220cb920 | ||
|
|
633b30269c | ||
|
|
8ad26267d8 | ||
|
|
7cfb125682 | ||
|
|
a2dc5de5ce | ||
|
|
4f41ff29ec | ||
|
|
13d95b8c19 | ||
|
|
335c13ab44 | ||
|
|
015cdc10a7 | ||
|
|
a5075ab28f | ||
|
|
2e3c3274c7 | ||
|
|
443c581955 | ||
|
|
5e41faf590 | ||
|
|
0579fb83d3 | ||
|
|
2864845d5b | ||
|
|
659dddace7 | ||
|
|
b096f30558 | ||
|
|
dcfb72143a | ||
|
|
c63bcd7706 | ||
|
|
bac306f9b3 | ||
|
|
a6fd96e97b | ||
|
|
7edd91f923 | ||
|
|
66df445bd5 | ||
|
|
5c03034bb4 | ||
|
|
0eab181d46 | ||
|
|
f6f95308c7 | ||
|
|
e93cad91b4 | ||
|
|
08ee61b179 | ||
|
|
0ab756665e | ||
|
|
3570cddce0 | ||
|
|
a2d9f1a5db | ||
|
|
0c6a214e55 | ||
|
|
095fcdbe69 | ||
|
|
3afb1d18f6 | ||
|
|
4526c84330 | ||
|
|
a9cc1cb4b9 | ||
|
|
7acf1eace3 | ||
|
|
bb2e8cb287 | ||
|
|
3b389259c1 | ||
|
|
310ce55b09 | ||
|
|
4662e3fec2 | ||
|
|
10fab9d415 | ||
|
|
acecf01cbe | ||
|
|
b95fb9cda4 | ||
|
|
23fd595582 | ||
|
|
468d05cb10 | ||
|
|
88c2e5d160 | ||
|
|
8cf491b802 | ||
|
|
5886e78299 | ||
|
|
591ef9781c | ||
|
|
52fef96c1c | ||
|
|
d5fd9c57ec | ||
|
|
8a7d6a1c13 | ||
|
|
1745c72981 | ||
|
|
b98b7646cb | ||
|
|
1446e925ec | ||
|
|
799cda1e3e | ||
|
|
d2e0231341 | ||
|
|
9e0aa655a0 | ||
|
|
e303c73d44 | ||
|
|
e5e8e66d6b | ||
|
|
0bb3381122 | ||
|
|
2c5bf2bd6b | ||
|
|
83763b00ae | ||
|
|
47fc795c97 | ||
|
|
6a59bf7af3 | ||
|
|
6f185570d6 | ||
|
|
8b8336d9d0 | ||
|
|
a1e1d6ed3f | ||
|
|
df3e209534 | ||
|
|
851408860e | ||
|
|
d92ea23b0e | ||
|
|
d83641db92 | ||
|
|
48f262e1f3 | ||
|
|
bc49a7db50 | ||
|
|
49975b461c | ||
|
|
3ccb635956 | ||
|
|
074a90e0ac | ||
|
|
526f927436 | ||
|
|
fae7fad915 | ||
|
|
56132ceb8b | ||
|
|
3c3fce0a5a | ||
|
|
8139384961 | ||
|
|
734cc94048 | ||
|
|
91dbb7770d | ||
|
|
707709837d | ||
|
|
e868e65208 | ||
|
|
b19ad2a13b | ||
|
|
98ccccf6ab | ||
|
|
0a6166c557 | ||
|
|
5ee00e52a8 | ||
|
|
ff2b4e0b2f | ||
|
|
8fdd8db51b | ||
|
|
f113eb7ba8 | ||
|
|
40bc25ed5f | ||
|
|
cd3eab5d35 | ||
|
|
7038e15ad2 | ||
|
|
c72abd29ec | ||
|
|
5efc3d2228 | ||
|
|
8ac3f83c79 | ||
|
|
b31b054d0c | ||
|
|
fb4a87f5ce | ||
|
|
049c73880b | ||
|
|
7b2c54f6a6 | ||
|
|
23bda39812 | ||
|
|
90af5801ee | ||
|
|
543c241bc6 | ||
|
|
19a58a3510 | ||
|
|
73c6fd18ed | ||
|
|
409297c6aa | ||
|
|
4272b9ea35 | ||
|
|
3c5c5b0bbc | ||
|
|
802b2d722e | ||
|
|
8f90d3f8ff | ||
|
|
f93cbbf508 | ||
|
|
2ba8756bd3 | ||
|
|
8013de56c9 | ||
|
|
4af9b22442 | ||
|
|
8a392170cf | ||
|
|
6385e053ae | ||
|
|
2789119882 | ||
|
|
3e0553c3e8 | ||
|
|
5e16ace1e7 | ||
|
|
0ca23cc4ca | ||
|
|
4949457001 | ||
|
|
8ae13fe823 | ||
|
|
fdf672092a | ||
|
|
48dd6ea826 | ||
|
|
68bb4f15e2 | ||
|
|
9c6d9ec22d | ||
|
|
ac075ace73 | ||
|
|
eb58774ee9 | ||
|
|
04613bf9a9 | ||
|
|
4194f1964d | ||
|
|
42c64c82d7 | ||
|
|
d44a8966f2 | ||
|
|
ff44330aa1 | ||
|
|
ed259e8168 | ||
|
|
444836975d | ||
|
|
3756fa93e4 | ||
|
|
064b52b306 | ||
|
|
648dc51474 | ||
|
|
7c0bc32759 | ||
|
|
d4b6cc5e4d | ||
|
|
75fca715e3 | ||
|
|
6e9a4a29ef | ||
|
|
78f643c50f | ||
|
|
2db4f1ba87 | ||
|
|
5741dbb78c | ||
|
|
88cf671465 | ||
|
|
29f082dea2 | ||
|
|
9a55968ed6 | ||
|
|
6ec6e25855 | ||
|
|
af48bc378b | ||
|
|
3f5ae4bcb9 | ||
|
|
c6624e9001 | ||
|
|
5530ce76e3 | ||
|
|
654b51e476 | ||
|
|
6428ac3624 | ||
|
|
c36450f436 | ||
|
|
dcea231249 | ||
|
|
f236119bd6 | ||
|
|
cb870fad85 | ||
|
|
44e2caedfc | ||
|
|
c0b5192296 | ||
|
|
ba43882c0b | ||
|
|
dfc706e16d | ||
|
|
25b03d4345 | ||
|
|
4e8f36d9a4 | ||
|
|
8f4d67ffa7 | ||
|
|
ad47277149 | ||
|
|
98c2a37ab6 | ||
|
|
f2fea47336 | ||
|
|
e274d3e2f9 | ||
|
|
36f4c3c02c | ||
|
|
ae891f8e55 | ||
|
|
c874dc9705 | ||
|
|
23bfbeb995 | ||
|
|
a75df03243 | ||
|
|
e44a2b5995 | ||
|
|
f23e7a9e49 | ||
|
|
e08f0cd308 | ||
|
|
1ebcfd8001 | ||
|
|
5168b044a3 | ||
|
|
931638b687 | ||
|
|
4d9bbcfd3f | ||
|
|
b0e3b26369 | ||
|
|
1e55f4065d | ||
|
|
518e8903a3 | ||
|
|
6e56a55f6f | ||
|
|
967071ad4e | ||
|
|
d23fcfaa5a | ||
|
|
a2f2306430 | ||
|
|
144b543091 | ||
|
|
e4ad8f512e | ||
|
|
591a5006da | ||
|
|
281ed6cfba | ||
|
|
7354a61184 | ||
|
|
04037f29e5 | ||
|
|
6b124bad89 | ||
|
|
54d99a4954 | ||
|
|
9947594366 | ||
|
|
d7ff934336 | ||
|
|
dc27856441 | ||
|
|
bd28682769 | ||
|
|
2060e62da4 | ||
|
|
70eac1e747 | ||
|
|
6f5a5fe066 | ||
|
|
8d24949202 | ||
|
|
f7b6237355 | ||
|
|
daf6f94c51 | ||
|
|
687d16e21b | ||
|
|
70571a9d34 | ||
|
|
4da8d8fe1e | ||
|
|
952cdc0e3f | ||
|
|
2de189d189 | ||
|
|
3edc6e83d7 | ||
|
|
31a5386cfe | ||
|
|
0198ebbbff | ||
|
|
ce3df8704d | ||
|
|
1b4ca3a684 | ||
|
|
e31c2c0955 | ||
|
|
4e389865b5 | ||
|
|
e5d8adf955 | ||
|
|
9a3181bc44 | ||
|
|
2f05c95422 | ||
|
|
bc5a3a2028 | ||
|
|
1fe91508f6 | ||
|
|
b9253c8191 | ||
|
|
7516acbc41 | ||
|
|
3b3d45da29 | ||
|
|
918d68d140 | ||
|
|
a3f56be299 | ||
|
|
f8f63890e5 | ||
|
|
9e997410ab | ||
|
|
529880642f | ||
|
|
d2590edab3 | ||
|
|
32eec68278 | ||
|
|
05ed8c9f4b | ||
|
|
bc281ee4d7 | ||
|
|
3e88314d0a | ||
|
|
141defd845 | ||
|
|
b9e49b181a | ||
|
|
102e447f78 | ||
|
|
f3de8435f1 |
+2
-1
@@ -9,4 +9,5 @@ SECRET='whyour'
|
||||
|
||||
QINIU_AK=''
|
||||
QINIU_SK=''
|
||||
QINIU_SCOPE=''
|
||||
QINIU_SCOPE=''
|
||||
TEMP=''
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: "\U0001F41E Bug report"
|
||||
description: Create a report to help us improve
|
||||
body:
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Qinglong version
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
|
||||
placeholder: Steps to reproduce
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What is expected?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actually-happening
|
||||
attributes:
|
||||
label: What is actually happening?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Info
|
||||
description: Output of `npx envinfo --system --binaries --browsers`
|
||||
render: shell
|
||||
placeholder: System, Binaries, Browsers
|
||||
- type: textarea
|
||||
id: additional-comments
|
||||
attributes:
|
||||
label: Any additional comments?
|
||||
description: e.g. some background/context of how you ran into this bug.
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Telegram Chat
|
||||
url: https://t.me/jiao_long
|
||||
about: Ask questions and discuss with other Qinglong users in real time.
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
|
||||
about: Use GitHub discussions for message-board style questions and discussions.
|
||||
@@ -0,0 +1,40 @@
|
||||
name: "\U0001F680 New feature proposal"
|
||||
description: Suggest an idea for this project
|
||||
labels: [":sparkles: feature request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for your interest in the project and taking the time to fill out this feature report!
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Clear and concise description of the problem
|
||||
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: suggested-solution
|
||||
attributes:
|
||||
label: Suggested solution
|
||||
description: "In module [xy] we could provide following implementation..."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternative
|
||||
attributes:
|
||||
label: Alternative
|
||||
description: Clear and concise description of any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Any other context or screenshots about the feature request here.
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
|
||||
required: true
|
||||
@@ -0,0 +1,31 @@
|
||||
## PR Type
|
||||
What kind of change does this PR introduce?
|
||||
|
||||
<!-- Please check the one that applies to this PR using "x". -->
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Feature
|
||||
- [ ] Code style update (formatting, local variables)
|
||||
- [ ] Refactoring (no functional changes, no api changes)
|
||||
- [ ] Other... Please describe:
|
||||
|
||||
|
||||
## What is the current behavior?
|
||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||
|
||||
Issue Number: N/A
|
||||
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
|
||||
## Does this PR introduce a breaking change?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
|
||||
|
||||
## Other information
|
||||
@@ -0,0 +1,7 @@
|
||||
# Comment to be posted to on PRs from first time contributors in your repository
|
||||
newPRWelcomeComment: |
|
||||
💖 Thanks for opening this pull request! 💖
|
||||
Please be patient and we will get back to you as soon as we can.
|
||||
# Comment to be posted to on pull requests merged by a first time user
|
||||
firstPRMergeComment: >
|
||||
Congrats on merging your first pull request! 🎉🎉🎉
|
||||
@@ -5,28 +5,55 @@ on:
|
||||
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
|
||||
- cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
to_gitlab:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
with:
|
||||
target_repo_url:
|
||||
git@gitlab.com:whyour/qinglong.git
|
||||
ssh_private_key:
|
||||
${{ secrets.GITLAB_SSH_PK }}
|
||||
|
||||
to_gitee:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
with:
|
||||
target_repo_url:
|
||||
git@gitee.com:whyour/qinglong.git
|
||||
ssh_private_key:
|
||||
${{ secrets.GITLAB_SSH_PK }}
|
||||
|
||||
build-static:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: '8.3.1'
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: build front and back
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm install
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build:front
|
||||
pnpm build:back
|
||||
|
||||
@@ -34,6 +61,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
||||
GITHUB_BRANCH: ${{ github.ref_name }}
|
||||
REPO_GITEE: git@gitee.com:whyour/qinglong-static.git
|
||||
REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git
|
||||
PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
||||
run: |
|
||||
mkdir -p tmp
|
||||
cd ./tmp
|
||||
@@ -44,6 +74,16 @@ jobs:
|
||||
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
||||
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
|
||||
git remote add gitee "${REPO_GITEE}"
|
||||
git remote add gitlab "${REPO_GITLAB}"
|
||||
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||
|
||||
build:
|
||||
needs: build-static
|
||||
|
||||
@@ -55,14 +95,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
- name: Set time zone
|
||||
uses: szenius/set-timezone@v1.0
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
timezoneLinux: "Asia/Shanghai"
|
||||
timezoneMacos: "Asia/Shanghai"
|
||||
timezoneWindows: "China Standard Time"
|
||||
version: '8.3.1'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup timezone
|
||||
uses: zcong1993/setup-timezone@master
|
||||
with:
|
||||
timezone: Asia/Shanghai
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
@@ -86,11 +129,14 @@ jobs:
|
||||
ghcr.io/${{ github.repository }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# nightly, master, pr-2, 1.2.3, 1.2, 1
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=schedule,pattern=nightly
|
||||
type=edge
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
@@ -100,7 +146,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -108,9 +154,16 @@ jobs:
|
||||
build-args: |
|
||||
MAINTAINER=${{ github.repository_owner }}
|
||||
QL_BRANCH=${{ github.ref_name }}
|
||||
SOURCE_COMMIT=${{ github.sha }}
|
||||
network: host
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
context: docker/
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=whyour/qinglong:cache
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
+2
-2
@@ -5,7 +5,6 @@
|
||||
/npm-debug.log*
|
||||
/yarn-error.log
|
||||
/yarn.lock
|
||||
/pnpm-lock.yaml
|
||||
/package-lock.json
|
||||
|
||||
# production
|
||||
@@ -29,4 +28,5 @@
|
||||
/db
|
||||
/manual_log
|
||||
/scripts
|
||||
/bak
|
||||
/bak
|
||||
/.tmp
|
||||
@@ -6,3 +6,27 @@ package.json
|
||||
.umi
|
||||
.umi-production
|
||||
.umi-test
|
||||
.history
|
||||
.tmp
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
static
|
||||
data
|
||||
DS_Store
|
||||
src/.umi
|
||||
src/.umi-production
|
||||
src/.umi-test
|
||||
.env.local
|
||||
.env
|
||||
history
|
||||
version.ts
|
||||
config
|
||||
log
|
||||
db
|
||||
manual_log
|
||||
scripts
|
||||
bak
|
||||
.tmp
|
||||
@@ -1,32 +1,34 @@
|
||||
import { defineConfig } from 'umi';
|
||||
import { defineConfig } from '@umijs/max';
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
|
||||
const baseUrl = process.env.QlBaseUrl || '/';
|
||||
export default defineConfig({
|
||||
hash: true,
|
||||
layout: false,
|
||||
jsMinifier: 'terser',
|
||||
antd: {},
|
||||
locale: {
|
||||
antd: true,
|
||||
title: true,
|
||||
baseNavigator: true,
|
||||
},
|
||||
outputPath: 'static/dist',
|
||||
nodeModulesTransform: {
|
||||
type: 'none',
|
||||
},
|
||||
fastRefresh: {},
|
||||
esbuild: {},
|
||||
webpack5: {},
|
||||
dynamicImport: {
|
||||
loading: '@/components/pageLoading',
|
||||
},
|
||||
favicon: '/images/g5.ico',
|
||||
fastRefresh: true,
|
||||
favicons: [`https://qn.whyour.cn/favicon.svg`],
|
||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||
proxy: {
|
||||
'/api/public': {
|
||||
[`${baseUrl}api/public`]: {
|
||||
target: 'http://127.0.0.1:5400/',
|
||||
changeOrigin: true,
|
||||
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
|
||||
},
|
||||
'/api': {
|
||||
[`${baseUrl}api`]: {
|
||||
target: 'http://127.0.0.1:5600/',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
pathRewrite: { [`^${baseUrl}api`]: '/api' },
|
||||
},
|
||||
},
|
||||
chainWebpack: (config) => {
|
||||
chainWebpack: ((config: any) => {
|
||||
config.plugin('compression-webpack-plugin').use(
|
||||
new CompressionPlugin({
|
||||
algorithm: 'gzip',
|
||||
@@ -35,13 +37,14 @@ export default defineConfig({
|
||||
minRatio: 0.6,
|
||||
}),
|
||||
);
|
||||
},
|
||||
}) as any,
|
||||
externals: {
|
||||
react: 'window.React',
|
||||
'react-dom': 'window.ReactDOM',
|
||||
},
|
||||
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',
|
||||
headScripts: [
|
||||
`./api/env.js`,
|
||||
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
|
||||
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
|
||||
],
|
||||
});
|
||||
|
||||
+79
-94
@@ -1,30 +1,29 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/whyour/qinglong">
|
||||
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">Green Dragon</h1>
|
||||
|
||||
<div align="center">
|
||||
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||
|
||||
Timed task management panel with python3, javaScript, shell, typescript support
|
||||
<h1 align="center">Qinglong</h1>
|
||||
|
||||
[![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]
|
||||
[简体中文](./README.md) | English
|
||||
|
||||
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
|
||||
[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||
|
||||
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
|
||||
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
|
||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
||||
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
|
||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
|
||||
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
||||
|
||||
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
||||
</div>
|
||||
|
||||
[](https://whyour.cn)
|
||||
|
||||
[简体中文](./README.md) | English
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
@@ -36,67 +35,32 @@ Timed task management panel with python3, javaScript, shell, typescript support
|
||||
- Support dark mode
|
||||
- Support cell phone operation
|
||||
|
||||
## Version
|
||||
### docker
|
||||
|
||||
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
||||
|
||||
```bash
|
||||
docker pull whyour/qinglong:latest
|
||||
docker pull whyour/qinglong:debian
|
||||
```
|
||||
|
||||
### npm
|
||||
|
||||
The npm version supports `debian/ubuntu/centos/alpine` systems and requires `node/python3` to be installed.
|
||||
|
||||
```bash
|
||||
npm i @whyour/qinglong
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Local Deployment
|
||||
|
||||
```bash
|
||||
# To be completed
|
||||
```
|
||||
|
||||
### Podman Deployment
|
||||
|
||||
1. podman installation
|
||||
|
||||
```bash
|
||||
https://podman.io/getting-started/installation
|
||||
```
|
||||
|
||||
2. start the container
|
||||
|
||||
```bash
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
1. docker installation
|
||||
|
||||
```bash
|
||||
sudo curl -sSL get.docker.com | sh
|
||||
```
|
||||
|
||||
2. configure domestic mirror sources
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/docker
|
||||
tee /etc/docker/daemon.json <<-'EOF'
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
||||
"https://ypzju6vq.mirror.aliyuncs.com",
|
||||
"https://registry.docker-cn.com",
|
||||
"http://hub-mirror.c.163.com",
|
||||
"https://docker.mirrors.ustc.edu.cn"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl restart docker
|
||||
```
|
||||
|
||||
3. start the container
|
||||
### Docker (Recommended)
|
||||
|
||||
```bash
|
||||
# curl -sSL get.docker.com | sh
|
||||
docker run -dit \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
@@ -104,17 +68,10 @@ docker run -dit \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### Docker-compose Deployment
|
||||
|
||||
1. docker-compose installation
|
||||
|
||||
```bash
|
||||
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
2. start the container
|
||||
### Docker-compose (Recommended)
|
||||
|
||||
```bash
|
||||
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
mkdir qinglong
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||
|
||||
@@ -124,6 +81,33 @@ docker-compose up -d
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Podman (Recommended)
|
||||
|
||||
```bash
|
||||
# https://podman.io/getting-started/installation
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### Local
|
||||
|
||||
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3 yourself
|
||||
|
||||
```bash
|
||||
npm install -g @whyour/qinglong
|
||||
qinglong
|
||||
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
|
||||
export QL_DIR=""
|
||||
export QL_DATA_DIR=""
|
||||
# Run again
|
||||
qinglong
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
1. built-in commands
|
||||
@@ -157,7 +141,11 @@ task <file_path> now
|
||||
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
|
||||
task <file_path> conc <env_name> <account_number>(Optional)
|
||||
# Specify the account to execute and run immediately regardless of whether a random delay is set
|
||||
task <file_path> desi <env_name> <account_number>
|
||||
task <file_path> desi <env_name> <account_number>
|
||||
# Set task timeout
|
||||
task -m <max_time> <file_path>
|
||||
# Print task log in real time, no need to carry this parameter when creating timed tasks
|
||||
task -l <file_path>
|
||||
```
|
||||
|
||||
2. parameter description
|
||||
@@ -172,16 +160,7 @@ task <file_path> desi <env_name> <account_number>
|
||||
* file_path: File path for task execution
|
||||
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
|
||||
* account_number: Specify the account number of an environment variable to be executed when the task is executed
|
||||
|
||||
## Links
|
||||
|
||||
- [nevinee](https://gitee.com/evine)
|
||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
- [Ant Design](https://ant.design)
|
||||
- [Ant Design Pro](https://pro.ant.design/)
|
||||
- [Umijs3.0](https://umijs.org)
|
||||
- [darkreader](https://github.com/darkreader/darkreader)
|
||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
|
||||
|
||||
## Development
|
||||
|
||||
@@ -190,16 +169,22 @@ $ git clone git@github.com:whyour/qinglong.git
|
||||
$ cd qinglong
|
||||
$ cp .env.example .env
|
||||
# Recommended use pnpm https://pnpm.io/zh/installation
|
||||
$ npm install -g pnpm
|
||||
$ npm install -g pnpm@8.3.1
|
||||
$ pnpm install
|
||||
$ pnpm start
|
||||
```
|
||||
|
||||
Open your browser and visit http://127.0.0.1:5700
|
||||
|
||||
## Communication
|
||||
## Links
|
||||
|
||||
[telegram channel](https://t.me/jiao_long)
|
||||
- [nevinee](https://gitee.com/evine)
|
||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
- [Ant Design](https://ant.design)
|
||||
- [Ant Design Pro](https://pro.ant.design/)
|
||||
- [Umijs](https://umijs.org)
|
||||
- [darkreader](https://github.com/darkreader/darkreader)
|
||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||
|
||||
## Name Origin
|
||||
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/whyour/qinglong">
|
||||
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
||||
</a>
|
||||
</p>
|
||||
<div align="center">
|
||||
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||
|
||||
<h1 align="center">青龙</h1>
|
||||
|
||||
<div align="center">
|
||||
简体中文 | [English](./README-en.md)
|
||||
|
||||
支持python3、javaScript、shell、typescript 的定时任务管理面板
|
||||
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台
|
||||
|
||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
|
||||
[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||
|
||||
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
|
||||
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
|
||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
||||
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
|
||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
|
||||
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
||||
|
||||
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
||||
</div>
|
||||
|
||||
[](https://whyour.cn)
|
||||
|
||||
简体中文 | [English](./README-en.md)
|
||||

|
||||
|
||||
## 功能
|
||||
|
||||
@@ -36,86 +37,45 @@
|
||||
- 支持暗黑模式
|
||||
- 支持手机端操作
|
||||
|
||||
## 版本
|
||||
### docker
|
||||
|
||||
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
||||
|
||||
```bash
|
||||
docker pull whyour/qinglong:latest
|
||||
docker pull whyour/qinglong:debian
|
||||
```
|
||||
|
||||
### npm
|
||||
|
||||
npm 版本支持 `debian/ubuntu/centos/alpine` 系统,需要自行安装 `node/python3`
|
||||
|
||||
```bash
|
||||
npm i @whyour/qinglong
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
### 本机部署
|
||||
|
||||
```bash
|
||||
# 待完善
|
||||
```
|
||||
|
||||
### podman 部署
|
||||
|
||||
1. podman 安装
|
||||
|
||||
```bash
|
||||
https://podman.io/getting-started/installation
|
||||
```
|
||||
|
||||
2. 启动容器
|
||||
|
||||
```bash
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### docker 部署
|
||||
|
||||
1. docker 安装
|
||||
|
||||
```bash
|
||||
sudo curl -sSL get.docker.com | sh
|
||||
```
|
||||
|
||||
2. 配置国内镜像源
|
||||
Configure domestic mirror sources
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/docker
|
||||
tee /etc/docker/daemon.json <<-'EOF'
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
||||
"https://ypzju6vq.mirror.aliyuncs.com",
|
||||
"https://registry.docker-cn.com",
|
||||
"http://hub-mirror.c.163.com",
|
||||
"https://docker.mirrors.ustc.edu.cn"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl restart docker
|
||||
```
|
||||
|
||||
3. 启动容器
|
||||
### docker (推荐)
|
||||
|
||||
```bash
|
||||
# curl -sSL get.docker.com | sh
|
||||
docker run -dit \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
-e QlBaseUrl="/" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### docker-compose 部署
|
||||
|
||||
1. docker-compose 安装
|
||||
|
||||
```bash
|
||||
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
2. 启动容器
|
||||
### docker-compose (推荐)
|
||||
|
||||
```bash
|
||||
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
mkdir qinglong
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||
|
||||
@@ -125,6 +85,35 @@ docker-compose up -d
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### podman (推荐)
|
||||
|
||||
```bash
|
||||
# https://podman.io/getting-started/installation
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
-e QlBaseUrl="/" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### 本机
|
||||
|
||||
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3
|
||||
|
||||
```bash
|
||||
npm install -g @whyour/qinglong
|
||||
qinglong
|
||||
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
|
||||
export QL_DIR=""
|
||||
export QL_DATA_DIR=""
|
||||
# 再次执行
|
||||
qinglong
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
1. 内置命令
|
||||
@@ -156,7 +145,11 @@ task <file_path> now
|
||||
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
||||
task <file_path> conc <env_name> <account_number>(可选的)
|
||||
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
||||
task <file_path> desi <env_name> <account_number>
|
||||
task <file_path> desi <env_name> <account_number>
|
||||
# 设置任务超时时间
|
||||
task -m <max_time> <file_path>
|
||||
# 实时打印任务日志,创建定时任务时,不用携带此参数
|
||||
task -l <file_path>
|
||||
```
|
||||
|
||||
2. 参数说明
|
||||
@@ -172,16 +165,7 @@ task <file_path> desi <env_name> <account_number>
|
||||
* file_path: 任务执行时的文件路径
|
||||
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||
|
||||
## 链接
|
||||
|
||||
- [nevinee](https://gitee.com/evine)
|
||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
- [Ant Design](https://ant.design)
|
||||
- [Ant Design Pro](https://pro.ant.design/)
|
||||
- [Umijs3.0](https://umijs.org)
|
||||
- [darkreader](https://github.com/darkreader/darkreader)
|
||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||
|
||||
## 开发
|
||||
|
||||
@@ -190,16 +174,22 @@ $ git clone git@github.com:whyour/qinglong.git
|
||||
$ cd qinglong
|
||||
$ cp .env.example .env
|
||||
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
||||
$ npm install -g pnpm
|
||||
$ npm install -g pnpm@8.3.1
|
||||
$ pnpm install
|
||||
$ pnpm start
|
||||
```
|
||||
|
||||
打开你的浏览器,访问 http://127.0.0.1:5700
|
||||
|
||||
## 交流
|
||||
## 链接
|
||||
|
||||
[telegram频道](https://t.me/jiao_long)
|
||||
- [nevinee](https://gitee.com/evine)
|
||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
- [Ant Design](https://ant.design)
|
||||
- [Ant Design Pro](https://pro.ant.design/)
|
||||
- [Umijs](https://umijs.org)
|
||||
- [darkreader](https://github.com/darkreader/darkreader)
|
||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||
|
||||
## 名称来源
|
||||
|
||||
|
||||
+4
-3
@@ -5,6 +5,7 @@ import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import { join } from 'path';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
@@ -41,11 +42,11 @@ export default (app: Router) => {
|
||||
}
|
||||
if (req.params.file.includes('sample')) {
|
||||
content = getFileContentByName(
|
||||
`${config.samplePath}${req.params.file}`,
|
||||
join(config.samplePath, req.params.file),
|
||||
);
|
||||
} else {
|
||||
content = getFileContentByName(
|
||||
`${config.configPath}${req.params.file}`,
|
||||
join(config.configPath, req.params.file),
|
||||
);
|
||||
}
|
||||
res.send({ code: 200, data: content });
|
||||
@@ -70,7 +71,7 @@ export default (app: Router) => {
|
||||
if (config.blackFileList.includes(name)) {
|
||||
res.send({ code: 403, message: '文件无法访问' });
|
||||
}
|
||||
const path = `${config.configPath}${name}`;
|
||||
const path = join(config.configPath, name);
|
||||
fs.writeFileSync(path, content);
|
||||
res.send({ code: 200, message: '保存成功' });
|
||||
} catch (e) {
|
||||
|
||||
+149
-1
@@ -2,6 +2,7 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import CronService from '../services/cron';
|
||||
import CronViewService from '../services/cronView';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import cron_parser from 'cron-parser';
|
||||
const route = Router();
|
||||
@@ -9,11 +10,141 @@ const route = Router();
|
||||
export default (app: Router) => {
|
||||
app.use('/crons', route);
|
||||
|
||||
route.get(
|
||||
'/views',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.list();
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/views',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
sorts: Joi.array().optional().allow(null),
|
||||
filters: Joi.array().optional(),
|
||||
filterRelation: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/views',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
sorts: Joi.array().optional().allow(null),
|
||||
filters: Joi.array().optional(),
|
||||
filterRelation: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
if (req.body.type === 1) {
|
||||
return res.send({ code: 400, message: '参数错误' });
|
||||
} else {
|
||||
const data = await cronViewService.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
}
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/views',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.remove(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/views/move',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
fromIndex: Joi.number().required(),
|
||||
toIndex: Joi.number().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.move(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/views/disable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.disabled(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/views/enable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronViewService = Container.get(CronViewService);
|
||||
const data = await cronViewService.enabled(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.crontabs(req.query.searchValue as string);
|
||||
const data = await cronService.crontabs(req.query as any);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -21,6 +152,21 @@ export default (app: Router) => {
|
||||
}
|
||||
});
|
||||
|
||||
route.get(
|
||||
'/detail',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.find(req.query as any);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/',
|
||||
celebrate({
|
||||
@@ -29,6 +175,7 @@ export default (app: Router) => {
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string().optional(),
|
||||
labels: Joi.array().optional(),
|
||||
sub_id: Joi.number().optional().allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -185,6 +332,7 @@ export default (app: Router) => {
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string().optional().allow(null),
|
||||
sub_id: Joi.number().optional().allow(null),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
|
||||
+50
-2
@@ -3,8 +3,22 @@ import { Container } from 'typedi';
|
||||
import EnvService from '../services/env';
|
||||
import { Logger } from 'winston';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import multer from 'multer';
|
||||
import config from '../config';
|
||||
import fs from 'fs';
|
||||
import { safeJSONParse } from '../config/util';
|
||||
const route = Router();
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, config.scriptPath);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
cb(null, file.originalname);
|
||||
},
|
||||
});
|
||||
const upload = multer({ storage: storage });
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/envs', route);
|
||||
|
||||
@@ -26,7 +40,9 @@ export default (app: Router) => {
|
||||
body: Joi.array().items(
|
||||
Joi.object({
|
||||
value: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
name: Joi.string()
|
||||
.required()
|
||||
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
|
||||
remarks: Joi.string().optional().allow(''),
|
||||
}),
|
||||
),
|
||||
@@ -94,7 +110,6 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.move(req.params.id, req.body);
|
||||
@@ -177,4 +192,37 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/upload',
|
||||
upload.single('env'),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
|
||||
const parseContent = safeJSONParse(fileContent);
|
||||
const data = Array.isArray(parseContent)
|
||||
? parseContent
|
||||
: [parseContent];
|
||||
if (data.every((x) => x.name && x.value)) {
|
||||
const result = await envService.create(
|
||||
data.map((x) => ({
|
||||
name: x.name,
|
||||
value: x.value,
|
||||
remarks: x.remarks,
|
||||
})),
|
||||
);
|
||||
return res.send({ code: 200, data: result });
|
||||
} else {
|
||||
return res.send({
|
||||
code: 400,
|
||||
message: '文件缺少name或者value字段,参考导出文件格式',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+31
-1
@@ -3,8 +3,9 @@ import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import config from '../config';
|
||||
import { getFileContentByName, readDirs } from '../config/util';
|
||||
import { emptyDir, getFileContentByName, readDirs } from '../config/util';
|
||||
import { join } from 'path';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
const route = Router();
|
||||
const blacklist = ['.tmp'];
|
||||
|
||||
@@ -45,4 +46,33 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
type: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let { filename, path, type } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.logPath, path, filename);
|
||||
if (type === 'directory') {
|
||||
emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+62
-16
@@ -4,6 +4,7 @@ import {
|
||||
readDirs,
|
||||
getLastModifyFilePath,
|
||||
readDir,
|
||||
emptyDir,
|
||||
} from '../config/util';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
@@ -77,12 +78,14 @@ export default (app: Router) => {
|
||||
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;
|
||||
};
|
||||
let { filename, path, content, originFilename, directory } =
|
||||
req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
content: string;
|
||||
originFilename: string;
|
||||
directory: string;
|
||||
};
|
||||
|
||||
if (!path) {
|
||||
path = config.scriptPath;
|
||||
@@ -91,7 +94,7 @@ export default (app: Router) => {
|
||||
path += '/';
|
||||
}
|
||||
if (!path.startsWith('/')) {
|
||||
path = `${config.scriptPath}${path}`;
|
||||
path = join(config.scriptPath, path);
|
||||
}
|
||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||
return res.send({
|
||||
@@ -105,15 +108,23 @@ export default (app: Router) => {
|
||||
return res.send({ code: 200 });
|
||||
}
|
||||
|
||||
if (directory) {
|
||||
fs.mkdirSync(join(path, directory), { recursive: true });
|
||||
return res.send({ code: 200 });
|
||||
}
|
||||
|
||||
if (!originFilename) {
|
||||
originFilename = filename;
|
||||
}
|
||||
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
|
||||
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
||||
const originFilePath = join(
|
||||
path,
|
||||
`${originFilename.replace(/\//g, '')}`,
|
||||
);
|
||||
const filePath = join(path, `${filename.replace(/\//g, '')}`);
|
||||
if (fs.existsSync(originFilePath)) {
|
||||
fs.copyFileSync(
|
||||
originFilePath,
|
||||
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
|
||||
join(config.bakPath, originFilename.replace(/\//g, '')),
|
||||
);
|
||||
if (filename !== originFilename) {
|
||||
fs.unlinkSync(originFilePath);
|
||||
@@ -159,17 +170,23 @@ export default (app: Router) => {
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
type: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path } = req.body as {
|
||||
let { filename, path, type } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
fs.unlinkSync(filePath);
|
||||
if (type === 'directory') {
|
||||
emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -190,7 +207,7 @@ export default (app: Router) => {
|
||||
let { filename } = req.body as {
|
||||
filename: string;
|
||||
};
|
||||
const filePath = `${config.scriptPath}${filename}`;
|
||||
const filePath = join(config.scriptPath, filename);
|
||||
// const stats = fs.statSync(filePath);
|
||||
// res.set({
|
||||
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
||||
@@ -238,23 +255,52 @@ export default (app: Router) => {
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
content: Joi.string().optional().allow(''),
|
||||
path: Joi.string().optional().allow(''),
|
||||
pid: Joi.number().optional().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, content, path } = req.body;
|
||||
let { filename, path, pid } = req.body;
|
||||
const { name, ext } = parse(filename);
|
||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||
const logPath = join(config.logPath, path, `${name}.swap`);
|
||||
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.stopScript(filePath);
|
||||
const result = await scriptService.stopScript(filePath, pid);
|
||||
emptyDir(logPath);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/rename',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
newFilename: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let { filename, path, type, newFilename } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
type: string;
|
||||
newFilename: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
const newPath = join(config.scriptPath, path, newFilename);
|
||||
fs.renameSync(filePath, newPath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,6 +49,9 @@ export default (app: Router) => {
|
||||
sub_after: Joi.string().optional().allow('').allow(null),
|
||||
schedule_type: Joi.string().required(),
|
||||
alias: Joi.string().required(),
|
||||
proxy: Joi.string().optional().allow('').allow(null),
|
||||
autoAddCron: Joi.boolean().optional().allow('').allow(null),
|
||||
autoDelCron: Joi.boolean().optional().allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -177,6 +180,9 @@ export default (app: Router) => {
|
||||
sub_before: Joi.string().optional().allow('').allow(null),
|
||||
sub_after: Joi.string().optional().allow('').allow(null),
|
||||
alias: Joi.string().required(),
|
||||
proxy: Joi.string().optional().allow('').allow(null),
|
||||
autoAddCron: Joi.boolean().optional().allow('').allow(null),
|
||||
autoDelCron: Joi.boolean().optional().allow('').allow(null),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
@@ -204,12 +210,16 @@ export default (app: Router) => {
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
query: Joi.object({
|
||||
force: Joi.boolean().optional(),
|
||||
t: Joi.number()
|
||||
})
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const data = await subscriptionService.remove(req.body);
|
||||
const data = await subscriptionService.remove(req.body, req.query);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
||||
+152
-14
@@ -7,7 +7,25 @@ import SystemService from '../services/system';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import UserService from '../services/user';
|
||||
import { EnvModel } from '../data/env';
|
||||
import {
|
||||
getUniqPath,
|
||||
handleLogPath,
|
||||
parseVersion,
|
||||
promiseExec,
|
||||
} from '../config/util';
|
||||
import dayjs from 'dayjs';
|
||||
import multer from 'multer';
|
||||
|
||||
const route = Router();
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, config.tmpPath);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
cb(null, 'data.tgz');
|
||||
},
|
||||
});
|
||||
const upload = multer({ storage: storage });
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/system', route);
|
||||
@@ -17,18 +35,14 @@ export default (app: Router) => {
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const envCount = await EnvModel.count();
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const version = currentVersionFile.match(versionRegx)![1];
|
||||
const { version, changeLog, changeLogLink, publishTime } =
|
||||
await parseVersion(config.versionFile);
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
Object.keys(authInfo).length === 2 &&
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'admin' &&
|
||||
envCount === 0
|
||||
authInfo.password === 'admin'
|
||||
) {
|
||||
isInitialized = false;
|
||||
}
|
||||
@@ -37,6 +51,10 @@ export default (app: Router) => {
|
||||
data: {
|
||||
isInitialized,
|
||||
version,
|
||||
publishTime: dayjs(publishTime).unix(),
|
||||
branch: process.env.QL_BRANCH || 'master',
|
||||
changeLog,
|
||||
changeLogLink,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -46,12 +64,12 @@ export default (app: Router) => {
|
||||
});
|
||||
|
||||
route.get(
|
||||
'/log/remove',
|
||||
'/config',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const data = await systemService.getLogRemoveFrequency();
|
||||
const data = await systemService.getSystemConfig();
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -60,19 +78,18 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/log/remove',
|
||||
'/config',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
frequency: Joi.number().required(),
|
||||
logRemoveFrequency: Joi.number().optional().allow(null),
|
||||
cronConcurrency: Joi.number().optional().allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateLogRemoveFrequency(
|
||||
req.body.frequency,
|
||||
);
|
||||
const result = await systemService.updateSystemConfig(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -108,6 +125,25 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/reload',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
type: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.reloadSystem(req.body.type);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/notify',
|
||||
celebrate({
|
||||
@@ -127,4 +163,106 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/command-run',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
command: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const command = req.body.command
|
||||
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
|
||||
let id = await promiseExec(idStr);
|
||||
const uniqPath = await getUniqPath(command, id);
|
||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||
const logPath = `${uniqPath}/${logTime}.log`;
|
||||
res.setHeader('Content-type', 'application/octet-stream');
|
||||
await systemService.run(
|
||||
{ ...req.body, logPath },
|
||||
{
|
||||
onStart: async (cp, startTime) => {
|
||||
res.setHeader('QL-Task-Pid', `${cp.pid}`);
|
||||
},
|
||||
onEnd: async (cp, endTime, diff) => {
|
||||
res.end();
|
||||
},
|
||||
onError: async (message: string) => {
|
||||
res.write(`\n${message}`);
|
||||
const absolutePath = await handleLogPath(logPath);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
},
|
||||
onLog: async (message: string) => {
|
||||
res.write(`\n${message}`);
|
||||
const absolutePath = await handleLogPath(logPath);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/command-stop',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
command: Joi.string().optional(),
|
||||
pid: Joi.number().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.stop(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/data/export',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
await systemService.exportData(res);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/data/import',
|
||||
upload.single('data'),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.importData();
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/log',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
await systemService.getSystemLog(res);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import config from '../config';
|
||||
const route = Router();
|
||||
|
||||
@@ -26,6 +27,10 @@ export default (app: Router) => {
|
||||
|
||||
route.post(
|
||||
'/login',
|
||||
rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 100,
|
||||
}),
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
username: Joi.string().required(),
|
||||
|
||||
+4
-1
@@ -17,10 +17,13 @@ async function startServer() {
|
||||
|
||||
const server = app
|
||||
.listen(config.port, () => {
|
||||
Logger.debug(`✌️ Back server launched on port ${config.port}`);
|
||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
||||
console.debug(`✌️ 后端服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const LOG_END_SYMBOL = ' ';
|
||||
|
||||
export const TASK_COMMAND = 'task';
|
||||
export const QL_COMMAND = 'ql';
|
||||
|
||||
export const TASK_PREFIX = `${TASK_COMMAND} `;
|
||||
export const QL_PREFIX = `${QL_COMMAND} `;
|
||||
+21
-4
@@ -1,6 +1,6 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { createRandomString } from './util';
|
||||
import { createRandomString } from './share';
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
@@ -11,15 +11,17 @@ if (!process.env.QL_DIR) {
|
||||
if (qlHomePath.endsWith('/static/')) {
|
||||
qlHomePath = path.join(qlHomePath, '../');
|
||||
}
|
||||
process.env.QL_DIR = qlHomePath;
|
||||
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
|
||||
}
|
||||
|
||||
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
|
||||
const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
|
||||
|
||||
const rootPath = process.env.QL_DIR as string;
|
||||
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
const shellPath = path.join(rootPath, 'shell/');
|
||||
const tmpPath = path.join(rootPath, '.tmp/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
@@ -27,6 +29,8 @@ const bakPath = path.join(dataPath, 'bak/');
|
||||
const logPath = path.join(dataPath, 'log/');
|
||||
const dbPath = path.join(dataPath, 'db/');
|
||||
const uploadPath = path.join(dataPath, 'upload/');
|
||||
const sshdPath = path.join(dataPath, 'ssh.d/');
|
||||
const systemLogPath = path.join(dataPath, 'syslog/');
|
||||
|
||||
const envFile = path.join(configPath, 'env.sh');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
@@ -40,7 +44,9 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
|
||||
const authError = '错误的用户名密码,请重试';
|
||||
const loginFaild = '请先登录!';
|
||||
const configString = 'config sample crontab shareCode diy';
|
||||
const versionFile = path.join(rootPath, 'src/version.ts');
|
||||
const versionFile = path.join(rootPath, 'version.yaml');
|
||||
const dataTgzFile = path.join(tmpPath, 'data.tgz');
|
||||
const shareShellFile = path.join(shellPath, 'share.sh');
|
||||
|
||||
if (envFound.error) {
|
||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||
@@ -58,6 +64,10 @@ export default {
|
||||
prefix: '/api',
|
||||
},
|
||||
rootPath,
|
||||
tmpPath,
|
||||
dataPath,
|
||||
dataTgzFile,
|
||||
shareShellFile,
|
||||
configString,
|
||||
loginFaild,
|
||||
authError,
|
||||
@@ -91,8 +101,15 @@ export default {
|
||||
'/api/system',
|
||||
'/api/user/init',
|
||||
'/api/user/notification/init',
|
||||
'/open/user/login',
|
||||
'/open/user/two-factor/login',
|
||||
'/open/system',
|
||||
'/open/user/init',
|
||||
'/open/user/notification/init',
|
||||
],
|
||||
versionFile,
|
||||
lastVersionFile,
|
||||
sqliteFile,
|
||||
sshdPath,
|
||||
systemLogPath,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
let pickedEnv: Record<string, string>;
|
||||
|
||||
function getPickedEnv() {
|
||||
if (pickedEnv) return pickedEnv;
|
||||
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
|
||||
pickedEnv = picked as Record<string, string>;
|
||||
return picked;
|
||||
}
|
||||
|
||||
export function serveEnv(_req: Request, res: Response) {
|
||||
res.type('.js');
|
||||
res.send(
|
||||
Object.entries(getPickedEnv())
|
||||
.map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
export function createRandomString(min: number, max: number): string {
|
||||
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
const english = [
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'i',
|
||||
'j',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'o',
|
||||
'p',
|
||||
'q',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
'u',
|
||||
'v',
|
||||
'w',
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
];
|
||||
const ENGLISH = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
];
|
||||
const special = ['-', '_'];
|
||||
const config = num.concat(english).concat(ENGLISH).concat(special);
|
||||
|
||||
const arr = [];
|
||||
arr.push(getOne(num));
|
||||
arr.push(getOne(english));
|
||||
arr.push(getOne(ENGLISH));
|
||||
arr.push(getOne(special));
|
||||
|
||||
const len = min + Math.floor(Math.random() * (max - min + 1));
|
||||
|
||||
for (let i = 4; i < len; i++) {
|
||||
arr.push(config[Math.floor(Math.random() * config.length)]);
|
||||
}
|
||||
|
||||
const newArr = [];
|
||||
for (let j = 0; j < len; j++) {
|
||||
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
||||
}
|
||||
|
||||
function getOne(arr: any[]) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
return newArr.join('');
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Subscription } from '../data/subscription';
|
||||
import isNil from 'lodash/isNil';
|
||||
|
||||
export function formatUrl(doc: Subscription) {
|
||||
let url = doc.url;
|
||||
let host = '';
|
||||
if (doc.type === 'private-repo') {
|
||||
if (doc.pull_type === 'ssh-key') {
|
||||
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
|
||||
url = doc.url!.replace(host, doc.alias);
|
||||
} else {
|
||||
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
|
||||
const { username, password } = doc.pull_option as any;
|
||||
url = doc.url!.replace(host, `${username}:${password}@${host}`);
|
||||
}
|
||||
}
|
||||
return { url, host };
|
||||
}
|
||||
|
||||
export function formatCommand(doc: Subscription, url?: string) {
|
||||
let command = `SUB_ID=${doc.id} ql `;
|
||||
let _url = url || formatUrl(doc).url;
|
||||
const {
|
||||
type,
|
||||
whitelist,
|
||||
blacklist,
|
||||
dependences,
|
||||
branch,
|
||||
extensions,
|
||||
proxy,
|
||||
autoAddCron,
|
||||
autoDelCron,
|
||||
} = doc;
|
||||
if (type === 'file') {
|
||||
command += `raw "${_url}"`;
|
||||
} else {
|
||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
||||
dependences || ''
|
||||
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${
|
||||
isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||
}
|
||||
return command;
|
||||
}
|
||||
+241
-103
@@ -3,6 +3,15 @@ import * as path from 'path';
|
||||
import got from 'got';
|
||||
import iconv from 'iconv-lite';
|
||||
import { exec } from 'child_process';
|
||||
import FormData from 'form-data';
|
||||
import psTreeFun from 'pstree.remy';
|
||||
import { promisify } from 'util';
|
||||
import { load } from 'js-yaml';
|
||||
import config from './index';
|
||||
import { TASK_COMMAND } from './const';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
export * from './share';
|
||||
|
||||
export function getFileContentByName(fileName: string) {
|
||||
if (fs.existsSync(fileName)) {
|
||||
@@ -30,91 +39,6 @@ export function getLastModifyFilePath(dir: string) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
export function createRandomString(min: number, max: number): string {
|
||||
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
const english = [
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'i',
|
||||
'j',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'o',
|
||||
'p',
|
||||
'q',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
'u',
|
||||
'v',
|
||||
'w',
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
];
|
||||
const ENGLISH = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
];
|
||||
const special = ['-', '_'];
|
||||
const config = num.concat(english).concat(ENGLISH).concat(special);
|
||||
|
||||
const arr = [];
|
||||
arr.push(getOne(num));
|
||||
arr.push(getOne(english));
|
||||
arr.push(getOne(ENGLISH));
|
||||
arr.push(getOne(special));
|
||||
|
||||
const len = min + Math.floor(Math.random() * (max - min + 1));
|
||||
|
||||
for (let i = 4; i < len; i++) {
|
||||
arr.push(config[Math.floor(Math.random() * config.length)]);
|
||||
}
|
||||
|
||||
const newArr = [];
|
||||
for (let j = 0; j < len; j++) {
|
||||
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
||||
}
|
||||
|
||||
function getOne(arr: any[]) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
return newArr.join('');
|
||||
}
|
||||
|
||||
export function getToken(req: any) {
|
||||
const { authorization = '' } = req.headers;
|
||||
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
||||
@@ -241,6 +165,18 @@ export async function createFile(file: string, data: string = '') {
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleLogPath(
|
||||
logPath: string,
|
||||
data: string = '',
|
||||
): Promise<string> {
|
||||
const absolutePath = path.resolve(config.logPath, logPath);
|
||||
const logFileExist = await fileExist(absolutePath);
|
||||
if (!logFileExist) {
|
||||
await createFile(absolutePath, data);
|
||||
}
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
export async function concurrentRun(
|
||||
fnList: Array<() => Promise<any>> = [],
|
||||
max = 5,
|
||||
@@ -283,14 +219,34 @@ enum FileType {
|
||||
'file',
|
||||
}
|
||||
|
||||
interface IFile {
|
||||
title: string;
|
||||
key: string;
|
||||
type: 'directory' | 'file';
|
||||
parent: string;
|
||||
mtime: number;
|
||||
size?: number;
|
||||
children?: IFile[];
|
||||
}
|
||||
|
||||
export function dirSort(a: IFile, b: IFile): number {
|
||||
if (a.type !== b.type) {
|
||||
return FileType[a.type] < FileType[b.type] ? -1 : 1
|
||||
} else if (a.mtime !== b.mtime) {
|
||||
return a.mtime > b.mtime ? -1 : 1
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function readDirs(
|
||||
dir: string,
|
||||
baseDir: string = '',
|
||||
blacklist: string[] = [],
|
||||
) {
|
||||
): IFile[] {
|
||||
const relativePath = path.relative(baseDir, dir);
|
||||
const files = fs.readdirSync(dir);
|
||||
const result: any = files
|
||||
const result: IFile[] = files
|
||||
.filter((x) => !blacklist.includes(x))
|
||||
.map((file: string) => {
|
||||
const subPath = path.join(dir, file);
|
||||
@@ -301,24 +257,22 @@ export function readDirs(
|
||||
title: file,
|
||||
key,
|
||||
type: 'directory',
|
||||
disabled: true,
|
||||
parent: relativePath,
|
||||
children: readDirs(subPath, baseDir).sort(
|
||||
(a: any, b: any) =>
|
||||
(FileType as any)[a.type] - (FileType as any)[b.type],
|
||||
),
|
||||
mtime: stats.mtime.getTime(),
|
||||
children: readDirs(subPath, baseDir).sort(dirSort),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: file,
|
||||
type: 'file',
|
||||
isLeaf: true,
|
||||
key,
|
||||
parent: relativePath,
|
||||
size: stats.size,
|
||||
mtime: stats.mtime.getTime(),
|
||||
};
|
||||
});
|
||||
return result.sort(
|
||||
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
|
||||
);
|
||||
return result.sort(dirSort);
|
||||
}
|
||||
|
||||
export function readDir(
|
||||
@@ -344,14 +298,198 @@ export function readDir(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function promiseExec(command: string): Promise<string> {
|
||||
export function emptyDir(path: string) {
|
||||
const files = fs.readdirSync(path);
|
||||
files.forEach((file) => {
|
||||
const filePath = `${path}/${file}`;
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(path);
|
||||
}
|
||||
|
||||
export async function promiseExec(command: string): Promise<string> {
|
||||
try {
|
||||
const { stderr, stdout } = await promisify(exec)(command, { maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' });
|
||||
return stdout || stderr;
|
||||
} catch (error) {
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function promiseExecSuccess(command: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await promisify(exec)(command, { maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' });
|
||||
return stdout || '';
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function parseHeaders(headers: string) {
|
||||
if (!headers) return {};
|
||||
|
||||
const parsed: any = {};
|
||||
let key;
|
||||
let val;
|
||||
let i;
|
||||
|
||||
headers &&
|
||||
headers.split('\n').forEach(function parser(line) {
|
||||
i = line.indexOf(':');
|
||||
key = line.substring(0, i).trim().toLowerCase();
|
||||
val = line.substring(i + 1).trim();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function parseBody(
|
||||
body: string,
|
||||
contentType:
|
||||
| 'application/json'
|
||||
| 'multipart/form-data'
|
||||
| 'application/x-www-form-urlencoded',
|
||||
) {
|
||||
if (!body) return '';
|
||||
|
||||
const parsed: any = {};
|
||||
let key;
|
||||
let val;
|
||||
let i;
|
||||
|
||||
body &&
|
||||
body.split('\n').forEach(function parser(line) {
|
||||
i = line.indexOf(':');
|
||||
key = line.substring(0, i).trim().toLowerCase();
|
||||
val = line.substring(i + 1).trim();
|
||||
|
||||
if (!key || parsed[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonValue = JSON.parse(val);
|
||||
parsed[key] = jsonValue;
|
||||
} catch (error) {
|
||||
parsed[key] = val;
|
||||
}
|
||||
});
|
||||
|
||||
switch (contentType) {
|
||||
case 'multipart/form-data':
|
||||
return Object.keys(parsed).reduce((p, c) => {
|
||||
p.append(c, parsed[c]);
|
||||
return p;
|
||||
}, new FormData());
|
||||
case 'application/x-www-form-urlencoded':
|
||||
return Object.keys(parsed).reduce((p, c) => {
|
||||
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
|
||||
});
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function psTree(pid: number): Promise<number[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
command,
|
||||
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
|
||||
(err, stdout, stderr) => {
|
||||
resolve(stdout || stderr || JSON.stringify(err));
|
||||
},
|
||||
);
|
||||
psTreeFun(pid, (err: any, pids: number[]) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(pids.filter((x) => !isNaN(x)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function killTask(pid: number) {
|
||||
const pids = await psTree(pid);
|
||||
// SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
|
||||
if (pids.length) {
|
||||
try {
|
||||
[pid, ...pids].forEach((x) => {
|
||||
process.kill(x, 2);
|
||||
});
|
||||
} catch (error) { }
|
||||
} else {
|
||||
process.kill(pid, 2);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPid(name: string) {
|
||||
const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
|
||||
const pid = await promiseExec(taskCommand);
|
||||
return pid ? Number(pid) : undefined;
|
||||
}
|
||||
|
||||
interface IVersion {
|
||||
version: string;
|
||||
changeLogLink: string;
|
||||
changeLog: string;
|
||||
publishTime: string;
|
||||
}
|
||||
|
||||
export async function parseVersion(path: string): Promise<IVersion> {
|
||||
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
|
||||
}
|
||||
|
||||
export async function parseContentVersion(content: string): Promise<IVersion> {
|
||||
return load(content) as IVersion;
|
||||
}
|
||||
|
||||
export async function getUniqPath(command: string, id: string): Promise<string> {
|
||||
if (/^\d+$/.test(id)) {
|
||||
id = `_${id}`;
|
||||
} else {
|
||||
id = '';
|
||||
}
|
||||
|
||||
const items = command.split(/ +/);
|
||||
let str = items[0];
|
||||
if (items[0] === TASK_COMMAND) {
|
||||
str = items[1];
|
||||
}
|
||||
|
||||
const dotIndex = str.lastIndexOf('.');
|
||||
|
||||
if (dotIndex !== -1) {
|
||||
str = str.slice(0, dotIndex);
|
||||
}
|
||||
|
||||
const slashIndex = str.lastIndexOf('/');
|
||||
|
||||
let tempStr = '';
|
||||
if (slashIndex !== -1) {
|
||||
tempStr = str.slice(0, slashIndex);
|
||||
const _slashIndex = tempStr.lastIndexOf('/');
|
||||
if (_slashIndex !== -1) {
|
||||
tempStr = tempStr.slice(_slashIndex + 1);
|
||||
}
|
||||
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
|
||||
}
|
||||
|
||||
return `${str}${id}`;
|
||||
}
|
||||
|
||||
export function safeJSONParse(value?: string) {
|
||||
if (!value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
Logger.error('[JSON.parse失败]', error)
|
||||
return {};
|
||||
}
|
||||
}
|
||||
+21
-2
@@ -1,10 +1,11 @@
|
||||
import { sequelize } from '.';
|
||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
import { NotificationInfo } from './notify';
|
||||
|
||||
export class AuthInfo {
|
||||
ip?: string;
|
||||
type: AuthDataType;
|
||||
info?: any;
|
||||
info?: AuthModelInfo;
|
||||
id?: number;
|
||||
|
||||
constructor(options: AuthInfo) {
|
||||
@@ -25,9 +26,27 @@ export enum AuthDataType {
|
||||
'authToken' = 'authToken',
|
||||
'notification' = 'notification',
|
||||
'removeLogFrequency' = 'removeLogFrequency',
|
||||
'systemConfig' = 'systemConfig',
|
||||
}
|
||||
|
||||
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
|
||||
export interface SystemConfigInfo {
|
||||
logRemoveFrequency?: number;
|
||||
cronConcurrency?: number;
|
||||
}
|
||||
|
||||
export interface LoginLogInfo {
|
||||
timestamp?: number;
|
||||
address?: string;
|
||||
ip?: string;
|
||||
platform?: string;
|
||||
status?: LoginStatus;
|
||||
}
|
||||
|
||||
export type AuthModelInfo = SystemConfigInfo &
|
||||
Partial<NotificationInfo> &
|
||||
LoginLogInfo;
|
||||
|
||||
export interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
|
||||
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
|
||||
ip: DataTypes.STRING,
|
||||
type: DataTypes.STRING,
|
||||
|
||||
+9
-6
@@ -17,15 +17,16 @@ export class Crontab {
|
||||
labels?: string[];
|
||||
last_running_time?: number;
|
||||
last_execution_time?: number;
|
||||
sub_id?: number;
|
||||
|
||||
constructor(options: Crontab) {
|
||||
this.name = options.name;
|
||||
this.command = options.command;
|
||||
this.command = options.command.trim();
|
||||
this.schedule = options.schedule;
|
||||
this.saved = options.saved;
|
||||
this.id = options.id;
|
||||
this.status =
|
||||
options.status && CrontabStatus[options.status]
|
||||
typeof options.status === 'number' && CrontabStatus[options.status]
|
||||
? options.status
|
||||
: CrontabStatus.idle;
|
||||
this.timestamp = new Date().toString();
|
||||
@@ -37,17 +38,18 @@ export class Crontab {
|
||||
this.labels = options.labels || [];
|
||||
this.last_running_time = options.last_running_time || 0;
|
||||
this.last_execution_time = options.last_execution_time || 0;
|
||||
this.sub_id = options.sub_id;
|
||||
}
|
||||
}
|
||||
|
||||
export enum CrontabStatus {
|
||||
'running',
|
||||
'idle',
|
||||
'running' = 0,
|
||||
'queued' = 0.5,
|
||||
'idle' = 1,
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
|
||||
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
||||
export interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
||||
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||
name: {
|
||||
unique: 'compositeIndex',
|
||||
@@ -72,4 +74,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||
labels: DataTypes.JSON,
|
||||
last_running_time: DataTypes.NUMBER,
|
||||
last_execution_time: DataTypes.NUMBER,
|
||||
sub_id: { type: DataTypes.NUMBER, allowNull: true },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { sequelize } from '.';
|
||||
import { DataTypes, Model } from 'sequelize';
|
||||
|
||||
export enum CronViewType {
|
||||
'系统' = 1,
|
||||
'个人',
|
||||
}
|
||||
|
||||
interface SortType {
|
||||
type: 'ASC' | 'DESC';
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface FilterType {
|
||||
property: string;
|
||||
operation: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class CrontabView {
|
||||
name?: string;
|
||||
id?: number;
|
||||
position?: number;
|
||||
isDisabled?: 1 | 0;
|
||||
filters?: FilterType[];
|
||||
sorts?: SortType[];
|
||||
filterRelation?: 'and' | 'or';
|
||||
type?: CronViewType;
|
||||
|
||||
constructor(options: CrontabView) {
|
||||
this.name = options.name;
|
||||
this.id = options.id;
|
||||
this.position = options.position;
|
||||
this.isDisabled = options.isDisabled || 0;
|
||||
this.filters = options.filters;
|
||||
this.sorts = options.sorts;
|
||||
this.filterRelation = options.filterRelation;
|
||||
this.type = options.type || CronViewType.个人;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CronViewInstance
|
||||
extends Model<CrontabView, CrontabView>,
|
||||
CrontabView {}
|
||||
export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
||||
'CrontabView',
|
||||
{
|
||||
name: {
|
||||
unique: 'name',
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
position: DataTypes.NUMBER,
|
||||
isDisabled: DataTypes.NUMBER,
|
||||
filters: DataTypes.JSON,
|
||||
sorts: DataTypes.JSON,
|
||||
filterRelation: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
type: DataTypes.NUMBER,
|
||||
},
|
||||
);
|
||||
+25
-9
@@ -4,18 +4,21 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
export class Dependence {
|
||||
timestamp?: string;
|
||||
id?: number;
|
||||
status?: DependenceStatus;
|
||||
type?: DependenceTypes;
|
||||
name?: number;
|
||||
status: DependenceStatus;
|
||||
type: DependenceTypes;
|
||||
name: string;
|
||||
log?: string[];
|
||||
remark?: string;
|
||||
|
||||
constructor(options: Dependence) {
|
||||
this.id = options.id;
|
||||
this.status = options.status || DependenceStatus.installing;
|
||||
this.status =
|
||||
typeof options.status === 'number' && DependenceStatus[options.status]
|
||||
? options.status
|
||||
: DependenceStatus.queued;
|
||||
this.type = options.type || DependenceTypes.nodejs;
|
||||
this.timestamp = new Date().toString();
|
||||
this.name = options.name;
|
||||
this.name = options.name.trim();
|
||||
this.log = options.log || [];
|
||||
this.remark = options.remark || '';
|
||||
}
|
||||
@@ -28,6 +31,7 @@ export enum DependenceStatus {
|
||||
'removing',
|
||||
'removed',
|
||||
'removeFailed',
|
||||
'queued',
|
||||
}
|
||||
|
||||
export enum DependenceTypes {
|
||||
@@ -38,17 +42,29 @@ export enum DependenceTypes {
|
||||
|
||||
export enum InstallDependenceCommandTypes {
|
||||
'pnpm add -g',
|
||||
'pip3 install',
|
||||
'apk add',
|
||||
'pip3 install --disable-pip-version-check --root-user-action=ignore',
|
||||
'apk add --no-check-certificate',
|
||||
}
|
||||
|
||||
export enum GetDependenceCommandTypes {
|
||||
'pnpm ls -g ',
|
||||
'pip3 show --disable-pip-version-check',
|
||||
'apk info -es',
|
||||
}
|
||||
|
||||
export enum versionDependenceCommandTypes {
|
||||
'@',
|
||||
'==',
|
||||
'=',
|
||||
}
|
||||
|
||||
export enum unInstallDependenceCommandTypes {
|
||||
'pnpm remove -g',
|
||||
'pip3 uninstall -y',
|
||||
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
||||
'apk del',
|
||||
}
|
||||
|
||||
interface DependenceInstance
|
||||
export interface DependenceInstance
|
||||
extends Model<Dependence, Dependence>,
|
||||
Dependence {}
|
||||
export const DependenceModel = sequelize.define<DependenceInstance>(
|
||||
|
||||
+9
-3
@@ -13,7 +13,10 @@ export class Env {
|
||||
constructor(options: Env) {
|
||||
this.value = options.value;
|
||||
this.id = options.id;
|
||||
this.status = options.status || EnvStatus.normal;
|
||||
this.status =
|
||||
typeof options.status === 'number' && EnvStatus[options.status]
|
||||
? options.status
|
||||
: EnvStatus.normal;
|
||||
this.timestamp = new Date().toString();
|
||||
this.position = options.position;
|
||||
this.name = options.name;
|
||||
@@ -26,9 +29,12 @@ export enum EnvStatus {
|
||||
'disabled',
|
||||
}
|
||||
|
||||
export const initEnvPosition = 9999999999;
|
||||
export const maxPosition = 9000000000000000;
|
||||
export const initPosition = 4500000000000000;
|
||||
export const stepPosition = 10000000000;
|
||||
export const minPosition = 100;
|
||||
|
||||
interface EnvInstance extends Model<Env, Env>, Env {}
|
||||
export interface EnvInstance extends Model<Env, Env>, Env {}
|
||||
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||
timestamp: DataTypes.STRING,
|
||||
|
||||
+14
-6
@@ -1,15 +1,23 @@
|
||||
import { Sequelize } from 'sequelize';
|
||||
import { Sequelize, Transaction } from 'sequelize';
|
||||
import config from '../config/index';
|
||||
import { join } from 'path';
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: `${config.dbPath}database.sqlite`,
|
||||
storage: join(config.dbPath, 'database.sqlite'),
|
||||
logging: false,
|
||||
pool: {
|
||||
max: 6,
|
||||
min: 0,
|
||||
idle: 30000,
|
||||
retry: {
|
||||
max: 10,
|
||||
match: ['SQLITE_BUSY: database is locked'],
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 2,
|
||||
idle: 30000,
|
||||
acquire: 30000,
|
||||
evict: 10000,
|
||||
},
|
||||
transactionType: Transaction.TYPES.IMMEDIATE,
|
||||
});
|
||||
|
||||
export type ResponseType<T> = { code: number; data?: T; message?: string };
|
||||
|
||||
+47
-2
@@ -1,16 +1,23 @@
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
|
||||
export enum NotificationMode {
|
||||
'gotify' = 'gotify',
|
||||
'goCqHttpBot' = 'goCqHttpBot',
|
||||
'serverChan' = 'serverChan',
|
||||
'pushDeer' = 'pushDeer',
|
||||
'bark' = 'bark',
|
||||
'chat' = 'chat',
|
||||
'telegramBot' = 'telegramBot',
|
||||
'dingtalkBot' = 'dingtalkBot',
|
||||
'weWorkBot' = 'weWorkBot',
|
||||
'weWorkApp' = 'weWorkApp',
|
||||
'aibotk' = 'aibotk',
|
||||
'iGot' = 'iGot',
|
||||
'pushPlus' = 'pushPlus',
|
||||
'email' = 'email',
|
||||
'pushMe' = 'pushMe',
|
||||
'feishu' = 'feishu',
|
||||
'webhook' = 'webhook',
|
||||
}
|
||||
|
||||
abstract class NotificationBaseInfo {
|
||||
@@ -35,11 +42,17 @@ export class ServerChanNotification extends NotificationBaseInfo {
|
||||
|
||||
export class PushDeerNotification extends NotificationBaseInfo {
|
||||
public pushDeerKey = '';
|
||||
public pushDeerUrl = '';
|
||||
}
|
||||
|
||||
export class ChatNotification extends NotificationBaseInfo {
|
||||
public chatUrl = '';
|
||||
public chatToken = '';
|
||||
}
|
||||
|
||||
export class BarkNotification extends NotificationBaseInfo {
|
||||
public barkPush = '';
|
||||
public barkIcon = 'http://qn.whyour.cn/logo.png';
|
||||
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
||||
public barkSound = '';
|
||||
public barkGroup = 'qinglong';
|
||||
}
|
||||
@@ -60,10 +73,18 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
|
||||
|
||||
export class WeWorkBotNotification extends NotificationBaseInfo {
|
||||
public weWorkBotKey = '';
|
||||
public weWorkOrigin = '';
|
||||
}
|
||||
|
||||
export class WeWorkAppNotification extends NotificationBaseInfo {
|
||||
public weWorkAppKey = '';
|
||||
public weWorkOrigin = '';
|
||||
}
|
||||
|
||||
export class AibotkNotification extends NotificationBaseInfo {
|
||||
public aibotkKey: string = '';
|
||||
public aibotkType: 'room' | 'contact' = 'room';
|
||||
public aibotkName: string = '';
|
||||
}
|
||||
|
||||
export class IGotNotification extends NotificationBaseInfo {
|
||||
@@ -81,16 +102,40 @@ export class EmailNotification extends NotificationBaseInfo {
|
||||
public emailPass: string = '';
|
||||
}
|
||||
|
||||
export class PushMeNotification extends NotificationBaseInfo {
|
||||
public pushMeKey: string = '';
|
||||
}
|
||||
|
||||
export class WebhookNotification extends NotificationBaseInfo {
|
||||
public webhookHeaders: string = '';
|
||||
public webhookBody: string = '';
|
||||
public webhookUrl: string = '';
|
||||
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
|
||||
public webhookContentType:
|
||||
| 'application/json'
|
||||
| 'multipart/form-data'
|
||||
| 'application/x-www-form-urlencoded' = 'application/json';
|
||||
}
|
||||
|
||||
export class LarkNotification extends NotificationBaseInfo {
|
||||
public larkKey = '';
|
||||
}
|
||||
|
||||
export interface NotificationInfo
|
||||
extends GoCqHttpBotNotification,
|
||||
GotifyNotification,
|
||||
ServerChanNotification,
|
||||
PushDeerNotification,
|
||||
ChatNotification,
|
||||
BarkNotification,
|
||||
TelegramBotNotification,
|
||||
DingtalkBotNotification,
|
||||
WeWorkBotNotification,
|
||||
WeWorkAppNotification,
|
||||
AibotkNotification,
|
||||
IGotNotification,
|
||||
PushPlusNotification,
|
||||
EmailNotification {}
|
||||
EmailNotification,
|
||||
PushMeNotification,
|
||||
WebhookNotification,
|
||||
LarkNotification {}
|
||||
|
||||
+1
-8
@@ -26,14 +26,7 @@ export interface AppToken {
|
||||
|
||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
||||
|
||||
export enum CrontabStatus {
|
||||
'running',
|
||||
'idle',
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
|
||||
interface AppInstance extends Model<App, App>, App {}
|
||||
export interface AppInstance extends Model<App, App>, App {}
|
||||
export const AppModel = sequelize.define<AppInstance>('App', {
|
||||
name: { type: DataTypes.STRING, unique: 'name' },
|
||||
scopes: DataTypes.JSON,
|
||||
|
||||
+2
-1
@@ -16,4 +16,5 @@ export type SockMessageType =
|
||||
| 'uninstallDependence'
|
||||
| 'updateSystemVersion'
|
||||
| 'manuallyRunScript'
|
||||
| 'runSubscriptionEnd';
|
||||
| 'runSubscriptionEnd'
|
||||
| 'reloadSystem';
|
||||
|
||||
@@ -28,14 +28,17 @@ export class Subscription {
|
||||
extensions?: string;
|
||||
sub_before?: string;
|
||||
sub_after?: string;
|
||||
proxy?: string;
|
||||
autoAddCron?: 1 | 0;
|
||||
autoDelCron?: 1 | 0;
|
||||
|
||||
constructor(options: Subscription) {
|
||||
this.id = options.id;
|
||||
this.name = options.name || options.alias;
|
||||
this.type = options.type;
|
||||
this.schedule = options.schedule;
|
||||
this.status =
|
||||
options.status && SubscriptionStatus[options.status]
|
||||
this.status = this.status =
|
||||
typeof options.status === 'number' && SubscriptionStatus[options.status]
|
||||
? options.status
|
||||
: SubscriptionStatus.idle;
|
||||
this.url = options.url;
|
||||
@@ -54,6 +57,9 @@ export class Subscription {
|
||||
this.extensions = options.extensions;
|
||||
this.sub_before = options.sub_before;
|
||||
this.sub_after = options.sub_after;
|
||||
this.proxy = options.proxy;
|
||||
this.autoAddCron = options.autoAddCron ? 1 : 0;
|
||||
this.autoDelCron = options.autoDelCron ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +70,7 @@ export enum SubscriptionStatus {
|
||||
'queued',
|
||||
}
|
||||
|
||||
interface SubscriptionInstance
|
||||
export interface SubscriptionInstance
|
||||
extends Model<Subscription, Subscription>,
|
||||
Subscription {}
|
||||
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
||||
@@ -102,5 +108,8 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
||||
log_path: DataTypes.STRING,
|
||||
schedule_type: DataTypes.STRING,
|
||||
alias: { type: DataTypes.STRING, unique: 'alias' },
|
||||
proxy: { type: DataTypes.STRING, allowNull: true },
|
||||
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||
},
|
||||
);
|
||||
|
||||
+24
-14
@@ -7,18 +7,28 @@ import linkDeps from './deps';
|
||||
import initTask from './initTask';
|
||||
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
await depInjectorLoader();
|
||||
Logger.info('✌️ Dependency Injector loaded');
|
||||
|
||||
await expressLoader({ app: expressApp });
|
||||
Logger.info('✌️ Express loaded');
|
||||
|
||||
await initData();
|
||||
Logger.info('✌️ init data loaded');
|
||||
|
||||
await linkDeps();
|
||||
Logger.info('✌️ link deps loaded');
|
||||
|
||||
initTask();
|
||||
Logger.info('✌️ init task loaded');
|
||||
try {
|
||||
depInjectorLoader();
|
||||
Logger.info('✌️ Dependency Injector loaded');
|
||||
console.log('✌️ Dependency Injector loaded');
|
||||
|
||||
expressLoader({ app: expressApp });
|
||||
Logger.info('✌️ Express loaded');
|
||||
console.log('✌️ Express loaded');
|
||||
|
||||
await initData();
|
||||
Logger.info('✌️ init data loaded');
|
||||
console.log('✌️ init data loaded');
|
||||
|
||||
await linkDeps();
|
||||
Logger.info('✌️ link deps loaded');
|
||||
console.log('✌️ link deps loaded');
|
||||
|
||||
initTask();
|
||||
Logger.info('✌️ init task loaded');
|
||||
console.log('✌️ init task loaded');
|
||||
} catch (error) {
|
||||
Logger.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed, ${error}`);
|
||||
console.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
+33
-9
@@ -6,10 +6,11 @@ import { CrontabModel } from '../data/cron';
|
||||
import { DependenceModel } from '../data/dependence';
|
||||
import { AppModel } from '../data/open';
|
||||
import { AuthModel } from '../data/auth';
|
||||
import { sequelize } from '../data';
|
||||
import { fileExist } from '../config/util';
|
||||
import { SubscriptionModel } from '../data/subscription';
|
||||
import { CrontabViewModel } from '../data/cronView';
|
||||
import config from '../config';
|
||||
import { sequelize } from '../data';
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
@@ -19,13 +20,35 @@ export default async () => {
|
||||
await AuthModel.sync();
|
||||
await EnvModel.sync();
|
||||
await SubscriptionModel.sync();
|
||||
await CrontabViewModel.sync();
|
||||
|
||||
// try {
|
||||
// const queryInterface = sequelize.getQueryInterface();
|
||||
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
|
||||
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
|
||||
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
|
||||
// } catch (error) { }
|
||||
// 初始化新增字段
|
||||
try {
|
||||
await sequelize.query(
|
||||
'alter table CrontabViews add column filterRelation VARCHAR(255)',
|
||||
);
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query(
|
||||
'alter table Subscriptions add column proxy VARCHAR(255)',
|
||||
);
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query('alter table CrontabViews add column type NUMBER');
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query(
|
||||
'alter table Subscriptions add column autoAddCron NUMBER',
|
||||
);
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query(
|
||||
'alter table Subscriptions add column autoDelCron NUMBER',
|
||||
);
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
|
||||
} catch (error) {}
|
||||
|
||||
// 2.10-2.11 升级
|
||||
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
||||
@@ -99,9 +122,10 @@ export default async () => {
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✌️ DB loaded');
|
||||
Logger.info('✌️ DB loaded');
|
||||
} catch (error) {
|
||||
Logger.info('✌️ DB load failed');
|
||||
Logger.info(error);
|
||||
console.error('✌️ DB load failed');
|
||||
Logger.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ async function linkCommand() {
|
||||
if (fs.existsSync(target)) {
|
||||
fs.unlinkSync(target);
|
||||
}
|
||||
fs.symlink(source, target, (err) => {});
|
||||
fs.symlink(source, target, (err) => { });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-24
@@ -3,39 +3,42 @@ import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import routes from '../api';
|
||||
import config from '../config';
|
||||
import jwt from 'express-jwt';
|
||||
import jwt, { UnauthorizedError } from 'express-jwt';
|
||||
import fs from 'fs';
|
||||
import { getPlatform, getToken } from '../config/util';
|
||||
import { getPlatform, getToken, safeJSONParse } from '../config/util';
|
||||
import Container from 'typedi';
|
||||
import OpenService from '../services/open';
|
||||
import rewrite from 'express-urlrewrite';
|
||||
import UserService from '../services/user';
|
||||
import handler from 'serve-handler';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { EnvModel } from '../data/env';
|
||||
import { errors } from 'celebrate';
|
||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
import { serveEnv } from '../config/serverEnv';
|
||||
import Logger from './logger';
|
||||
|
||||
export default ({ app }: { app: Application }) => {
|
||||
app.enable('trust proxy');
|
||||
app.use(cors());
|
||||
app.get(`${config.api.prefix}/env.js`, serveEnv);
|
||||
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
||||
next();
|
||||
} else {
|
||||
return handler(req, res, {
|
||||
public: 'static/dist',
|
||||
rewrites: [{ source: '**', destination: '/index.html' }],
|
||||
});
|
||||
}
|
||||
});
|
||||
app.use(
|
||||
'/api/public',
|
||||
createProxyMiddleware({
|
||||
target: `http://0.0.0.0:${config.publicPort}/api`,
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '/api/public': '' },
|
||||
logProvider: () => Logger
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||
|
||||
app.use(
|
||||
jwt({
|
||||
secret: config.secret as string,
|
||||
secret: config.secret,
|
||||
algorithms: ['HS384'],
|
||||
}).unless({
|
||||
path: [...config.apiWhiteList, /^\/open\//],
|
||||
@@ -75,22 +78,24 @@ export default ({ app }: { app: Application }) => {
|
||||
if (
|
||||
!headerToken &&
|
||||
originPath &&
|
||||
config.apiWhiteList.includes(originPath) &&
|
||||
originPath !== '/api/crons/status'
|
||||
config.apiWhiteList.includes(originPath)
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
if (data) {
|
||||
const { token = '', tokens = {} } = JSON.parse(data);
|
||||
if (data && headerToken) {
|
||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
const err: any = new Error('UnauthorizedError');
|
||||
err.status = 401;
|
||||
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
|
||||
const errorMessage = headerToken
|
||||
? 'jwt malformed'
|
||||
: 'No authorization token was found';
|
||||
const err = new UnauthorizedError(errorCode, { message: errorMessage });
|
||||
next(err);
|
||||
});
|
||||
|
||||
@@ -106,8 +111,7 @@ export default ({ app }: { app: Application }) => {
|
||||
if (
|
||||
Object.keys(authInfo).length === 2 &&
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'admin' &&
|
||||
envCount === 0
|
||||
authInfo.password === 'admin'
|
||||
) {
|
||||
isInitialized = false;
|
||||
}
|
||||
@@ -168,8 +172,6 @@ export default ({ app }: { app: Application }) => {
|
||||
},
|
||||
);
|
||||
|
||||
app.use(Sentry.Handlers.errorHandler());
|
||||
|
||||
app.use(
|
||||
(
|
||||
err: Error & { status: number },
|
||||
|
||||
+28
-15
@@ -4,38 +4,51 @@ import { Container } from 'typedi';
|
||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import CronService from '../services/cron';
|
||||
import EnvService from '../services/env';
|
||||
import _ from 'lodash';
|
||||
import { DependenceModel } from '../data/dependence';
|
||||
import { DependenceModel, DependenceStatus } from '../data/dependence';
|
||||
import { Op } from 'sequelize';
|
||||
import config from '../config';
|
||||
import { CrontabViewModel, CronViewType } from '../data/cronView';
|
||||
import { initPosition } from '../data/env';
|
||||
|
||||
export default async () => {
|
||||
const cronService = Container.get(CronService);
|
||||
const envService = Container.get(EnvService);
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
|
||||
// 初始化新增默认全部任务视图
|
||||
CrontabViewModel.findAll({
|
||||
where: { type: CronViewType.系统, name: '全部任务' },
|
||||
raw: true,
|
||||
}).then((docs) => {
|
||||
if (docs.length === 0) {
|
||||
CrontabViewModel.create({
|
||||
name: '全部任务',
|
||||
type: CronViewType.系统,
|
||||
position: initPosition / 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化更新所有任务状态为空闲
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle },
|
||||
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
|
||||
);
|
||||
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
|
||||
|
||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||
DependenceModel.findAll({
|
||||
where: {},
|
||||
order: [['type', 'DESC']],
|
||||
order: [
|
||||
['type', 'DESC'],
|
||||
['createdAt', 'DESC'],
|
||||
],
|
||||
raw: true,
|
||||
}).then(async (docs) => {
|
||||
const groups = _.groupBy(docs, 'type');
|
||||
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
|
||||
for (const key of keys) {
|
||||
const group = groups[key];
|
||||
const depIds = group.map((x) => x.id);
|
||||
await dependenceService.reInstall(depIds as number[]);
|
||||
}
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.queued, log: [] },
|
||||
{ where: { id: docs.map((x) => x.id!) } },
|
||||
);
|
||||
dependenceService.installDependenceOneByOne(docs);
|
||||
});
|
||||
|
||||
// 初始化时执行一次所有的ql repo 任务
|
||||
// 初始化时执行一次所有的 ql repo 任务
|
||||
CrontabModel.findAll({
|
||||
where: {
|
||||
isDisabled: { [Op.ne]: 1 },
|
||||
|
||||
@@ -5,7 +5,7 @@ import dotenv from 'dotenv';
|
||||
import Logger from './logger';
|
||||
import { fileExist } from '../config/util';
|
||||
|
||||
const rootPath = process.env.QL_DIR as string;;
|
||||
const rootPath = process.env.QL_DIR as string;
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
@@ -13,12 +13,22 @@ const logPath = path.join(dataPath, 'log/');
|
||||
const uploadPath = path.join(dataPath, 'upload/');
|
||||
const bakPath = path.join(dataPath, 'bak/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const tmpPath = path.join(logPath, '.tmp/');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const authConfigFile = path.join(configPath, 'auth.json');
|
||||
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
||||
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
|
||||
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
|
||||
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
|
||||
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
|
||||
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
|
||||
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
|
||||
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
|
||||
const TaskAfterFile = path.join(configPath, 'task_after.sh');
|
||||
const homedir = os.homedir();
|
||||
const sshPath = path.resolve(homedir, '.ssh');
|
||||
const sshdPath = path.join(dataPath, 'ssh.d');
|
||||
const systemLogPath = path.join(dataPath, 'syslog');
|
||||
|
||||
export default async () => {
|
||||
const authFileExist = await fileExist(authConfigFile);
|
||||
@@ -29,19 +39,18 @@ export default async () => {
|
||||
const uploadDirExist = await fileExist(uploadPath);
|
||||
const sshDirExist = await fileExist(sshPath);
|
||||
const bakDirExist = await fileExist(bakPath);
|
||||
const sshdDirExist = await fileExist(sshdPath);
|
||||
const systemLogDirExist = await fileExist(systemLogPath);
|
||||
const tmpDirExist = await fileExist(tmpPath);
|
||||
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
|
||||
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
|
||||
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
|
||||
const TaskAfterFileExist = await fileExist(TaskAfterFile);
|
||||
|
||||
if (!configDirExist) {
|
||||
fs.mkdirSync(configPath);
|
||||
}
|
||||
|
||||
if (!authFileExist) {
|
||||
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||
}
|
||||
|
||||
if (!confFileExist) {
|
||||
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
||||
}
|
||||
|
||||
if (!scriptDirExist) {
|
||||
fs.mkdirSync(scriptPath);
|
||||
}
|
||||
@@ -50,6 +59,10 @@ export default async () => {
|
||||
fs.mkdirSync(logPath);
|
||||
}
|
||||
|
||||
if (!tmpDirExist) {
|
||||
fs.mkdirSync(tmpPath);
|
||||
}
|
||||
|
||||
if (!uploadDirExist) {
|
||||
fs.mkdirSync(uploadPath);
|
||||
}
|
||||
@@ -62,7 +75,41 @@ export default async () => {
|
||||
fs.mkdirSync(bakPath);
|
||||
}
|
||||
|
||||
if (!sshdDirExist) {
|
||||
fs.mkdirSync(sshdPath);
|
||||
}
|
||||
|
||||
if (!systemLogDirExist) {
|
||||
fs.mkdirSync(systemLogPath);
|
||||
}
|
||||
|
||||
// 初始化文件
|
||||
if (!authFileExist) {
|
||||
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||
}
|
||||
|
||||
if (!confFileExist) {
|
||||
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
||||
}
|
||||
|
||||
if (!scriptNotifyJsFileExist) {
|
||||
fs.writeFileSync(scriptNotifyJsFile, fs.readFileSync(sampleNotifyJsFile));
|
||||
}
|
||||
|
||||
if (!scriptNotifyPyFileExist) {
|
||||
fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile));
|
||||
}
|
||||
|
||||
if (!TaskBeforeFileExist) {
|
||||
fs.writeFileSync(TaskBeforeFile, fs.readFileSync(sampleTaskShellFile));
|
||||
}
|
||||
|
||||
if (!TaskAfterFileExist) {
|
||||
fs.writeFileSync(TaskAfterFile, fs.readFileSync(sampleTaskShellFile));
|
||||
}
|
||||
|
||||
dotenv.config({ path: confFile });
|
||||
|
||||
Logger.info('✌️ Init file down');
|
||||
console.log('✌️ Init file down');
|
||||
};
|
||||
|
||||
+17
-20
@@ -1,10 +1,10 @@
|
||||
import { Container } from 'typedi';
|
||||
import _ from 'lodash';
|
||||
import SystemService from '../services/system';
|
||||
import ScheduleService from '../services/schedule';
|
||||
import SubscriptionService from '../services/subscription';
|
||||
import config from '../config';
|
||||
import { fileExist } from '../config/util';
|
||||
import { join } from 'path';
|
||||
|
||||
export default async () => {
|
||||
const systemService = Container.get(SystemService);
|
||||
@@ -12,43 +12,40 @@ export default async () => {
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
|
||||
// 生成内置token
|
||||
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
|
||||
const tokenFile = `${config.rootPath}/static/build/token.js`;
|
||||
let tokenCommand = `tsx ${join(config.rootPath, 'back/token.ts')}`;
|
||||
const tokenFile = join(config.rootPath, 'static/build/token.js');
|
||||
|
||||
if (await fileExist(tokenFile)) {
|
||||
tokenCommand = `node ${tokenFile}`;
|
||||
}
|
||||
const cron = {
|
||||
id: 'token',
|
||||
id: NaN,
|
||||
name: '生成token',
|
||||
command: tokenCommand,
|
||||
};
|
||||
scheduleService.createIntervalTask(cron as any, {
|
||||
await scheduleService.cancelIntervalTask(cron);
|
||||
scheduleService.createIntervalTask(cron, {
|
||||
days: 28,
|
||||
runImmediately: true,
|
||||
});
|
||||
|
||||
// 运行删除日志任务
|
||||
const data = await systemService.getLogRemoveFrequency();
|
||||
if (data && data.info && data.info.frequency) {
|
||||
const cron = {
|
||||
id: data.id,
|
||||
const data = await systemService.getSystemConfig();
|
||||
if (data && data.info && data.info.logRemoveFrequency) {
|
||||
const rmlogCron = {
|
||||
id: data.id as number,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${data.info.frequency}`,
|
||||
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||
};
|
||||
scheduleService.createIntervalTask(cron, {
|
||||
days: data.info.frequency,
|
||||
runImmediately: true,
|
||||
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||
scheduleService.createIntervalTask(rmlogCron, {
|
||||
days: data.info.logRemoveFrequency,
|
||||
});
|
||||
}
|
||||
|
||||
// 运行所有订阅
|
||||
await subscriptionService.setSshConfig();
|
||||
const subs = await subscriptionService.list();
|
||||
for (const sub of subs) {
|
||||
await subscriptionService.handleTask(
|
||||
sub,
|
||||
!sub.is_disabled,
|
||||
true,
|
||||
!sub.is_disabled,
|
||||
);
|
||||
subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
|
||||
}
|
||||
};
|
||||
|
||||
+27
-21
@@ -1,32 +1,38 @@
|
||||
import winston from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
import config from '../config';
|
||||
import path from 'path';
|
||||
|
||||
const transports = [];
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
transports.push(new winston.transports.Console());
|
||||
} else {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.cli(),
|
||||
winston.format.splat(),
|
||||
),
|
||||
}),
|
||||
);
|
||||
const levelMap: Record<string, string> = {
|
||||
info: '🔵',
|
||||
warn: '🟡',
|
||||
error: '🔴',
|
||||
debug: '🔶'
|
||||
}
|
||||
|
||||
const customFormat = winston.format.combine(
|
||||
winston.format.splat(),
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
winston.format.align(),
|
||||
winston.format.printf((i) => `[${levelMap[i.level]}${i.level}] [${[i.timestamp]}]: ${i.message}`),
|
||||
);
|
||||
|
||||
const defaultOptions = {
|
||||
format: customFormat,
|
||||
datePattern: "YYYY-MM-DD",
|
||||
maxSize: "20m",
|
||||
maxFiles: "7d",
|
||||
};
|
||||
|
||||
const LoggerInstance = winston.createLogger({
|
||||
level: config.logs.level,
|
||||
levels: winston.config.npm.levels,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
}),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
winston.format.json(),
|
||||
),
|
||||
transports,
|
||||
transports: [
|
||||
new winston.transports.DailyRotateFile({
|
||||
filename: path.join(config.systemLogPath, '%DATE%.log'),
|
||||
...defaultOptions,
|
||||
})
|
||||
],
|
||||
});
|
||||
|
||||
export default LoggerInstance;
|
||||
|
||||
+15
-3
@@ -2,20 +2,32 @@ import { Application } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import * as Tracing from '@sentry/tracing';
|
||||
import Logger from './logger';
|
||||
import config from '../config';
|
||||
import fs from 'fs';
|
||||
import { parseVersion } from '../config/util';
|
||||
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
const { version } = await parseVersion(config.versionFile);
|
||||
|
||||
export default ({ expressApp }: { expressApp: Application }) => {
|
||||
Sentry.init({
|
||||
ignoreErrors: [
|
||||
/SequelizeUniqueConstraintError/i,
|
||||
/Validation error/i,
|
||||
/UnauthorizedError/i,
|
||||
/celebrate request validation failed/i,
|
||||
],
|
||||
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
||||
integrations: [
|
||||
new Sentry.Integrations.Http({ tracing: true }),
|
||||
new Tracing.Integrations.Express({ app: expressApp }),
|
||||
],
|
||||
|
||||
tracesSampleRate: 1.0,
|
||||
tracesSampleRate: 0.8,
|
||||
release: version,
|
||||
});
|
||||
|
||||
expressApp.use(Sentry.Handlers.requestHandler());
|
||||
expressApp.use(Sentry.Handlers.tracingHandler());
|
||||
|
||||
Logger.info('✌️ Sentry loaded');
|
||||
console.log('✌️ Sentry loaded');
|
||||
};
|
||||
|
||||
+30
-4
@@ -5,12 +5,38 @@ import Sock from './sock';
|
||||
export default async ({ server }: { server: Server }) => {
|
||||
await Sock({ server });
|
||||
Logger.info('✌️ Sock loaded');
|
||||
let exitTime = 0;
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
Logger.info('✌️ Server need close');
|
||||
process.on('SIGINT', (singal) => {
|
||||
Logger.warn(`Server need close, singal ${singal}`);
|
||||
console.warn(`Server need close, singal ${singal}`);
|
||||
exitTime++;
|
||||
if (exitTime >= 3) {
|
||||
Logger.warn('Forcing server close');
|
||||
console.warn('Forcing server close');
|
||||
clearTimeout(timer);
|
||||
process.exit(1);
|
||||
}
|
||||
server.close(() => {
|
||||
Logger.info('✌️ Server closed');
|
||||
process.exit(0);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
process.exit();
|
||||
}, 15000);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
Logger.error('Uncaught exception:', error);
|
||||
console.error('Uncaught exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
Logger.error('Unhandled rejection:', reason, promise);
|
||||
console.error('Unhandled rejection:', reason, promise);
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Container } from 'typedi';
|
||||
import SockService from '../services/sock';
|
||||
import config from '../config/index';
|
||||
import fs from 'fs';
|
||||
import { getPlatform } from '../config/util';
|
||||
import { getPlatform, safeJSONParse } from '../config/util';
|
||||
|
||||
export default async ({ server }: { server: Server }) => {
|
||||
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||
@@ -20,7 +20,7 @@ export default async ({ server }: { server: Server }) => {
|
||||
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||
if (data) {
|
||||
const { token = '', tokens = {} } = JSON.parse(data);
|
||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||
if (headerToken === token || tokens[platform] === headerToken) {
|
||||
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
||||
sockService.addClient(conn);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package com.ql.cron;
|
||||
|
||||
service Cron {
|
||||
rpc addCron(AddCronRequest) returns (AddCronResponse);
|
||||
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
|
||||
}
|
||||
|
||||
message ICron {
|
||||
string id = 1;
|
||||
string schedule = 2;
|
||||
string command = 3;
|
||||
}
|
||||
|
||||
message AddCronRequest { repeated ICron crons = 1; }
|
||||
|
||||
message AddCronResponse {}
|
||||
|
||||
message DeleteCronRequest { repeated string ids = 1; }
|
||||
|
||||
message DeleteCronResponse {}
|
||||
@@ -0,0 +1,482 @@
|
||||
/* eslint-disable */
|
||||
import {
|
||||
CallOptions,
|
||||
ChannelCredentials,
|
||||
Client,
|
||||
ClientOptions,
|
||||
ClientUnaryCall,
|
||||
handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
Metadata,
|
||||
ServiceError,
|
||||
UntypedServiceImplementation,
|
||||
} from '@grpc/grpc-js';
|
||||
import _m0 from 'protobufjs/minimal';
|
||||
|
||||
export const protobufPackage = 'com.ql.cron';
|
||||
|
||||
export interface ICron {
|
||||
id: string;
|
||||
schedule: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
export interface AddCronRequest {
|
||||
crons: ICron[];
|
||||
}
|
||||
|
||||
export interface AddCronResponse {}
|
||||
|
||||
export interface DeleteCronRequest {
|
||||
ids: string[];
|
||||
}
|
||||
|
||||
export interface DeleteCronResponse {}
|
||||
|
||||
function createBaseICron(): ICron {
|
||||
return { id: '', schedule: '', command: '' };
|
||||
}
|
||||
|
||||
export const ICron = {
|
||||
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
if (message.id !== '') {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
if (message.schedule !== '') {
|
||||
writer.uint32(18).string(message.schedule);
|
||||
}
|
||||
if (message.command !== '') {
|
||||
writer.uint32(26).string(message.command);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseICron();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag != 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
case 2:
|
||||
if (tag != 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.schedule = reader.string();
|
||||
continue;
|
||||
case 3:
|
||||
if (tag != 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.command = reader.string();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ICron {
|
||||
return {
|
||||
id: isSet(object.id) ? String(object.id) : '',
|
||||
schedule: isSet(object.schedule) ? String(object.schedule) : '',
|
||||
command: isSet(object.command) ? String(object.command) : '',
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: ICron): unknown {
|
||||
const obj: any = {};
|
||||
message.id !== undefined && (obj.id = message.id);
|
||||
message.schedule !== undefined && (obj.schedule = message.schedule);
|
||||
message.command !== undefined && (obj.command = message.command);
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
|
||||
return ICron.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
|
||||
const message = createBaseICron();
|
||||
message.id = object.id ?? '';
|
||||
message.schedule = object.schedule ?? '';
|
||||
message.command = object.command ?? '';
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseAddCronRequest(): AddCronRequest {
|
||||
return { crons: [] };
|
||||
}
|
||||
|
||||
export const AddCronRequest = {
|
||||
encode(
|
||||
message: AddCronRequest,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
for (const v of message.crons) {
|
||||
ICron.encode(v!, writer.uint32(10).fork()).ldelim();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAddCronRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag != 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.crons.push(ICron.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): AddCronRequest {
|
||||
return {
|
||||
crons: Array.isArray(object?.crons)
|
||||
? object.crons.map((e: any) => ICron.fromJSON(e))
|
||||
: [],
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: AddCronRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.crons) {
|
||||
obj.crons = message.crons.map((e) => (e ? ICron.toJSON(e) : undefined));
|
||||
} else {
|
||||
obj.crons = [];
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(
|
||||
base?: I,
|
||||
): AddCronRequest {
|
||||
return AddCronRequest.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(
|
||||
object: I,
|
||||
): AddCronRequest {
|
||||
const message = createBaseAddCronRequest();
|
||||
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseAddCronResponse(): AddCronResponse {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const AddCronResponse = {
|
||||
encode(
|
||||
_: AddCronResponse,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAddCronResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(_: any): AddCronResponse {
|
||||
return {};
|
||||
},
|
||||
|
||||
toJSON(_: AddCronResponse): unknown {
|
||||
const obj: any = {};
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(
|
||||
base?: I,
|
||||
): AddCronResponse {
|
||||
return AddCronResponse.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(
|
||||
_: I,
|
||||
): AddCronResponse {
|
||||
const message = createBaseAddCronResponse();
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseDeleteCronRequest(): DeleteCronRequest {
|
||||
return { ids: [] };
|
||||
}
|
||||
|
||||
export const DeleteCronRequest = {
|
||||
encode(
|
||||
message: DeleteCronRequest,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
for (const v of message.ids) {
|
||||
writer.uint32(10).string(v!);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseDeleteCronRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag != 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.ids.push(reader.string());
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): DeleteCronRequest {
|
||||
return {
|
||||
ids: Array.isArray(object?.ids)
|
||||
? object.ids.map((e: any) => String(e))
|
||||
: [],
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: DeleteCronRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.ids) {
|
||||
obj.ids = message.ids.map((e) => e);
|
||||
} else {
|
||||
obj.ids = [];
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
|
||||
base?: I,
|
||||
): DeleteCronRequest {
|
||||
return DeleteCronRequest.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
|
||||
object: I,
|
||||
): DeleteCronRequest {
|
||||
const message = createBaseDeleteCronRequest();
|
||||
message.ids = object.ids?.map((e) => e) || [];
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseDeleteCronResponse(): DeleteCronResponse {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const DeleteCronResponse = {
|
||||
encode(
|
||||
_: DeleteCronResponse,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseDeleteCronResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(_: any): DeleteCronResponse {
|
||||
return {};
|
||||
},
|
||||
|
||||
toJSON(_: DeleteCronResponse): unknown {
|
||||
const obj: any = {};
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
|
||||
base?: I,
|
||||
): DeleteCronResponse {
|
||||
return DeleteCronResponse.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
|
||||
_: I,
|
||||
): DeleteCronResponse {
|
||||
const message = createBaseDeleteCronResponse();
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
export type CronService = typeof CronService;
|
||||
export const CronService = {
|
||||
addCron: {
|
||||
path: '/com.ql.cron.Cron/addCron',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: AddCronRequest) =>
|
||||
Buffer.from(AddCronRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
|
||||
responseSerialize: (value: AddCronResponse) =>
|
||||
Buffer.from(AddCronResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
|
||||
},
|
||||
delCron: {
|
||||
path: '/com.ql.cron.Cron/delCron',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: DeleteCronRequest) =>
|
||||
Buffer.from(DeleteCronRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
|
||||
responseSerialize: (value: DeleteCronResponse) =>
|
||||
Buffer.from(DeleteCronResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export interface CronServer extends UntypedServiceImplementation {
|
||||
addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;
|
||||
delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;
|
||||
}
|
||||
|
||||
export interface CronClient extends Client {
|
||||
addCron(
|
||||
request: AddCronRequest,
|
||||
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
addCron(
|
||||
request: AddCronRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
addCron(
|
||||
request: AddCronRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
delCron(
|
||||
request: DeleteCronRequest,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: DeleteCronResponse,
|
||||
) => void,
|
||||
): ClientUnaryCall;
|
||||
delCron(
|
||||
request: DeleteCronRequest,
|
||||
metadata: Metadata,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: DeleteCronResponse,
|
||||
) => void,
|
||||
): ClientUnaryCall;
|
||||
delCron(
|
||||
request: DeleteCronRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: DeleteCronResponse,
|
||||
) => void,
|
||||
): ClientUnaryCall;
|
||||
}
|
||||
|
||||
export const CronClient = makeGenericClientConstructor(
|
||||
CronService,
|
||||
'com.ql.cron.Cron',
|
||||
) as unknown as {
|
||||
new (
|
||||
address: string,
|
||||
credentials: ChannelCredentials,
|
||||
options?: Partial<ClientOptions>,
|
||||
): CronClient;
|
||||
service: typeof CronService;
|
||||
};
|
||||
|
||||
type Builtin =
|
||||
| Date
|
||||
| Function
|
||||
| Uint8Array
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {}
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
export type Exact<P, I extends P> = P extends Builtin
|
||||
? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
|
||||
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
|
||||
};
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package com.ql.health;
|
||||
|
||||
message HealthCheckRequest {
|
||||
string service = 1;
|
||||
}
|
||||
|
||||
message HealthCheckResponse {
|
||||
enum ServingStatus {
|
||||
UNKNOWN = 0;
|
||||
SERVING = 1;
|
||||
NOT_SERVING = 2;
|
||||
SERVICE_UNKNOWN = 3;
|
||||
}
|
||||
ServingStatus status = 1;
|
||||
}
|
||||
|
||||
service Health {
|
||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
/* eslint-disable */
|
||||
import {
|
||||
CallOptions,
|
||||
ChannelCredentials,
|
||||
Client,
|
||||
ClientOptions,
|
||||
ClientReadableStream,
|
||||
ClientUnaryCall,
|
||||
handleServerStreamingCall,
|
||||
handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
Metadata,
|
||||
ServiceError,
|
||||
UntypedServiceImplementation,
|
||||
} from '@grpc/grpc-js';
|
||||
import _m0 from 'protobufjs/minimal';
|
||||
|
||||
export const protobufPackage = 'com.ql.health';
|
||||
|
||||
export interface HealthCheckRequest {
|
||||
service: string;
|
||||
}
|
||||
|
||||
export interface HealthCheckResponse {
|
||||
status: HealthCheckResponse_ServingStatus;
|
||||
}
|
||||
|
||||
export enum HealthCheckResponse_ServingStatus {
|
||||
UNKNOWN = 0,
|
||||
SERVING = 1,
|
||||
NOT_SERVING = 2,
|
||||
SERVICE_UNKNOWN = 3,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function healthCheckResponse_ServingStatusFromJSON(
|
||||
object: any,
|
||||
): HealthCheckResponse_ServingStatus {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case 'UNKNOWN':
|
||||
return HealthCheckResponse_ServingStatus.UNKNOWN;
|
||||
case 1:
|
||||
case 'SERVING':
|
||||
return HealthCheckResponse_ServingStatus.SERVING;
|
||||
case 2:
|
||||
case 'NOT_SERVING':
|
||||
return HealthCheckResponse_ServingStatus.NOT_SERVING;
|
||||
case 3:
|
||||
case 'SERVICE_UNKNOWN':
|
||||
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
|
||||
case -1:
|
||||
case 'UNRECOGNIZED':
|
||||
default:
|
||||
return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function healthCheckResponse_ServingStatusToJSON(
|
||||
object: HealthCheckResponse_ServingStatus,
|
||||
): string {
|
||||
switch (object) {
|
||||
case HealthCheckResponse_ServingStatus.UNKNOWN:
|
||||
return 'UNKNOWN';
|
||||
case HealthCheckResponse_ServingStatus.SERVING:
|
||||
return 'SERVING';
|
||||
case HealthCheckResponse_ServingStatus.NOT_SERVING:
|
||||
return 'NOT_SERVING';
|
||||
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
|
||||
return 'SERVICE_UNKNOWN';
|
||||
case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
|
||||
default:
|
||||
return 'UNRECOGNIZED';
|
||||
}
|
||||
}
|
||||
|
||||
function createBaseHealthCheckRequest(): HealthCheckRequest {
|
||||
return { service: '' };
|
||||
}
|
||||
|
||||
export const HealthCheckRequest = {
|
||||
encode(
|
||||
message: HealthCheckRequest,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
if (message.service !== '') {
|
||||
writer.uint32(10).string(message.service);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHealthCheckRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag != 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.service = reader.string();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): HealthCheckRequest {
|
||||
return { service: isSet(object.service) ? String(object.service) : '' };
|
||||
},
|
||||
|
||||
toJSON(message: HealthCheckRequest): unknown {
|
||||
const obj: any = {};
|
||||
message.service !== undefined && (obj.service = message.service);
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
|
||||
base?: I,
|
||||
): HealthCheckRequest {
|
||||
return HealthCheckRequest.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
|
||||
object: I,
|
||||
): HealthCheckRequest {
|
||||
const message = createBaseHealthCheckRequest();
|
||||
message.service = object.service ?? '';
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseHealthCheckResponse(): HealthCheckResponse {
|
||||
return { status: 0 };
|
||||
}
|
||||
|
||||
export const HealthCheckResponse = {
|
||||
encode(
|
||||
message: HealthCheckResponse,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
if (message.status !== 0) {
|
||||
writer.uint32(8).int32(message.status);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHealthCheckResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag != 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.status = reader.int32() as any;
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): HealthCheckResponse {
|
||||
return {
|
||||
status: isSet(object.status)
|
||||
? healthCheckResponse_ServingStatusFromJSON(object.status)
|
||||
: 0,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: HealthCheckResponse): unknown {
|
||||
const obj: any = {};
|
||||
message.status !== undefined &&
|
||||
(obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
|
||||
base?: I,
|
||||
): HealthCheckResponse {
|
||||
return HealthCheckResponse.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
|
||||
object: I,
|
||||
): HealthCheckResponse {
|
||||
const message = createBaseHealthCheckResponse();
|
||||
message.status = object.status ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
export type HealthService = typeof HealthService;
|
||||
export const HealthService = {
|
||||
check: {
|
||||
path: '/com.ql.health.Health/Check',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: HealthCheckRequest) =>
|
||||
Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
||||
responseSerialize: (value: HealthCheckResponse) =>
|
||||
Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
||||
},
|
||||
watch: {
|
||||
path: '/com.ql.health.Health/Watch',
|
||||
requestStream: false,
|
||||
responseStream: true,
|
||||
requestSerialize: (value: HealthCheckRequest) =>
|
||||
Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
||||
responseSerialize: (value: HealthCheckResponse) =>
|
||||
Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export interface HealthServer extends UntypedServiceImplementation {
|
||||
check: handleUnaryCall<HealthCheckRequest, HealthCheckResponse>;
|
||||
watch: handleServerStreamingCall<HealthCheckRequest, HealthCheckResponse>;
|
||||
}
|
||||
|
||||
export interface HealthClient extends Client {
|
||||
check(
|
||||
request: HealthCheckRequest,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: HealthCheckResponse,
|
||||
) => void,
|
||||
): ClientUnaryCall;
|
||||
check(
|
||||
request: HealthCheckRequest,
|
||||
metadata: Metadata,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: HealthCheckResponse,
|
||||
) => void,
|
||||
): ClientUnaryCall;
|
||||
check(
|
||||
request: HealthCheckRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: HealthCheckResponse,
|
||||
) => void,
|
||||
): ClientUnaryCall;
|
||||
watch(
|
||||
request: HealthCheckRequest,
|
||||
options?: Partial<CallOptions>,
|
||||
): ClientReadableStream<HealthCheckResponse>;
|
||||
watch(
|
||||
request: HealthCheckRequest,
|
||||
metadata?: Metadata,
|
||||
options?: Partial<CallOptions>,
|
||||
): ClientReadableStream<HealthCheckResponse>;
|
||||
}
|
||||
|
||||
export const HealthClient = makeGenericClientConstructor(
|
||||
HealthService,
|
||||
'com.ql.health.Health',
|
||||
) as unknown as {
|
||||
new (
|
||||
address: string,
|
||||
credentials: ChannelCredentials,
|
||||
options?: Partial<ClientOptions>,
|
||||
): HealthClient;
|
||||
service: typeof HealthService;
|
||||
};
|
||||
|
||||
type Builtin =
|
||||
| Date
|
||||
| Function
|
||||
| Uint8Array
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {}
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
export type Exact<P, I extends P> = P extends Builtin
|
||||
? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
|
||||
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
|
||||
};
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
+17
-12
@@ -1,20 +1,22 @@
|
||||
import express from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import Logger from './loaders/logger';
|
||||
import config from './config';
|
||||
import { HealthClient } from './protos/health';
|
||||
import { credentials } from '@grpc/grpc-js';
|
||||
|
||||
const app = express();
|
||||
const client = new HealthClient(
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
credentials.createInsecure(),
|
||||
);
|
||||
|
||||
app.get('/api/public/panel/log', (req, res) => {
|
||||
exec(
|
||||
'pm2 logs panel --lines 500 --nostream --timestamp',
|
||||
(err, stdout, stderr) => {
|
||||
if (err || stderr) {
|
||||
return res.send({ code: 400, message: (err && err.message) || stderr });
|
||||
}
|
||||
return res.send({ code: 200, data: stdout });
|
||||
},
|
||||
);
|
||||
app.get('/api/health', (req, res) => {
|
||||
client.check({ service: 'cron' }, (err, response) => {
|
||||
if (err) {
|
||||
return res.status(200).send({ code: 500, error: err });
|
||||
}
|
||||
return res.status(200).send({ code: 200, data: response });
|
||||
});
|
||||
});
|
||||
|
||||
app
|
||||
@@ -22,9 +24,12 @@ app
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
await require('./loaders/db').default();
|
||||
|
||||
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`);
|
||||
Logger.debug(`✌️ 公共服务启动成功!`);
|
||||
console.debug(`✌️ 公共服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import schedule from 'node-schedule';
|
||||
import express from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import Logger from './loaders/logger';
|
||||
import { CrontabModel, CrontabStatus } from './data/cron';
|
||||
import config from './config';
|
||||
|
||||
const app = express();
|
||||
|
||||
const run = async () => {
|
||||
CrontabModel.findAll({ where: {} })
|
||||
.then((docs) => {
|
||||
if (docs && docs.length > 0) {
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const task = docs[i];
|
||||
const _schedule = task.schedule && task.schedule.split(/ +/);
|
||||
if (
|
||||
_schedule &&
|
||||
_schedule.length > 5 &&
|
||||
task.status !== CrontabStatus.disabled &&
|
||||
!task.isDisabled
|
||||
) {
|
||||
schedule.scheduleJob(task.schedule, function () {
|
||||
let command = task.command as string;
|
||||
if (!command.includes('task ') && !command.includes('ql ')) {
|
||||
command = `task ${command}`;
|
||||
}
|
||||
exec(command);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
Logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
|
||||
app
|
||||
.listen(config.cronPort, async () => {
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
await require('./loaders/db').default();
|
||||
|
||||
await run();
|
||||
Logger.info(`
|
||||
################################################
|
||||
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
||||
################################################
|
||||
`);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||
import { AddCronRequest, AddCronResponse } from '../protos/cron';
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import { scheduleStacks } from './data';
|
||||
import { runCron } from '../shared/runCron';
|
||||
import { QL_PREFIX, TASK_PREFIX } from '../config/const';
|
||||
import Logger from '../loaders/logger';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const addCron = (
|
||||
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
|
||||
callback: sendUnaryData<AddCronResponse>,
|
||||
) => {
|
||||
for (const item of call.request.crons) {
|
||||
const { id, schedule, command } = item;
|
||||
if (scheduleStacks.has(id)) {
|
||||
scheduleStacks.get(id)?.cancel();
|
||||
}
|
||||
|
||||
let cmdStr = command.trim();
|
||||
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
|
||||
cmdStr = `${TASK_PREFIX}${cmdStr}`;
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
'[schedule][创建定时任务], 任务ID: %s, cron: %s, 执行命令: %s',
|
||||
id,
|
||||
schedule,
|
||||
command,
|
||||
);
|
||||
|
||||
scheduleStacks.set(
|
||||
id,
|
||||
nodeSchedule.scheduleJob(id, schedule, async () => {
|
||||
Logger.info(
|
||||
`[schedule][准备运行任务] 命令: ${cmdStr}`,
|
||||
);
|
||||
runCron(`ID=${id} ${cmdStr}`);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
callback(null, null);
|
||||
};
|
||||
|
||||
export { addCron };
|
||||
@@ -0,0 +1,40 @@
|
||||
import { credentials } from '@grpc/grpc-js';
|
||||
import {
|
||||
AddCronRequest,
|
||||
AddCronResponse,
|
||||
CronClient,
|
||||
DeleteCronRequest,
|
||||
DeleteCronResponse,
|
||||
} from '../protos/cron';
|
||||
import config from '../config';
|
||||
|
||||
class Client {
|
||||
private client = new CronClient(
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
credentials.createInsecure(),
|
||||
);
|
||||
|
||||
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.addCron({ crons: request }, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.delCron({ ids: request }, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Client();
|
||||
@@ -0,0 +1,6 @@
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import { ToadScheduler } from 'toad-scheduler';
|
||||
|
||||
export const scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||
|
||||
export const intervalSchedule = new ToadScheduler();
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
|
||||
import { scheduleStacks } from './data';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
const delCron = (
|
||||
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
|
||||
callback: sendUnaryData<DeleteCronResponse>,
|
||||
) => {
|
||||
for (const id of call.request.ids) {
|
||||
if (scheduleStacks.has(id)) {
|
||||
Logger.info(
|
||||
'[schedule][取消定时任务], 任务ID: %s',
|
||||
id,
|
||||
);
|
||||
scheduleStacks.get(id)?.cancel();
|
||||
scheduleStacks.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, null);
|
||||
};
|
||||
|
||||
export { delCron };
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
|
||||
import config from '../config';
|
||||
import { promiseExec } from '../config/util';
|
||||
|
||||
const check = async (
|
||||
call: ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,
|
||||
callback: sendUnaryData<HealthCheckResponse>,
|
||||
) => {
|
||||
switch (call.request.service) {
|
||||
case 'cron':
|
||||
const res = await promiseExec(
|
||||
`curl -s http://0.0.0.0:${config.port}/api/system`,
|
||||
);
|
||||
|
||||
if (res.includes('200')) {
|
||||
return callback(null, { status: 1 });
|
||||
}
|
||||
|
||||
const panelErrLog = await promiseExec(
|
||||
`tail -n 300 ~/.pm2/logs/panel-error.log`,
|
||||
);
|
||||
const scheduleErrLog = await promiseExec(
|
||||
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
|
||||
);
|
||||
return callback(
|
||||
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`.trim()),
|
||||
);
|
||||
|
||||
default:
|
||||
return callback(null, { status: 1 });
|
||||
}
|
||||
};
|
||||
|
||||
export { check };
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Server, ServerCredentials } from '@grpc/grpc-js';
|
||||
import { CronService } from '../protos/cron';
|
||||
import { addCron } from './addCron';
|
||||
import { delCron } from './delCron';
|
||||
import { HealthService } from '../protos/health';
|
||||
import { check } from './health';
|
||||
import config from '../config';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
const server = new Server();
|
||||
server.addService(HealthService, { check });
|
||||
server.addService(CronService, { addCron, delCron });
|
||||
server.bindAsync(
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
ServerCredentials.createInsecure(),
|
||||
(err, port) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
server.start();
|
||||
Logger.debug(`✌️ 定时服务启动成功!`);
|
||||
console.debug(`✌️ 定时服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
},
|
||||
);
|
||||
+326
-244
@@ -2,23 +2,26 @@ import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import { exec, execSync, spawn } from 'child_process';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
|
||||
import { getFileContentByName, fileExist, killTask, getUniqPath, safeJSONParse } from '../config/util';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { Op } from 'sequelize';
|
||||
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
|
||||
import path from 'path';
|
||||
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
||||
import cronClient from '../schedule/client';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import { spawn } from 'cross-spawn';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||
|
||||
private isSixCron(cron: Crontab) {
|
||||
const { schedule } = cron;
|
||||
if (schedule?.split(/ +/).length === 6) {
|
||||
if (Number(schedule?.split(/ +/).length) > 5) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -28,7 +31,12 @@ export default class CronService {
|
||||
const tab = new Crontab(payload);
|
||||
tab.saved = false;
|
||||
const doc = await this.insert(tab);
|
||||
await this.set_crontab(this.isSixCron(doc));
|
||||
if (this.isSixCron(doc)) {
|
||||
await cronClient.addCron([
|
||||
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command },
|
||||
]);
|
||||
}
|
||||
await this.set_crontab();
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -37,9 +45,26 @@ export default class CronService {
|
||||
}
|
||||
|
||||
public async update(payload: Crontab): Promise<Crontab> {
|
||||
payload.saved = false;
|
||||
const newDoc = await this.updateDb(payload);
|
||||
await this.set_crontab(this.isSixCron(newDoc));
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new Crontab({ ...doc, ...payload });
|
||||
tab.saved = false;
|
||||
const newDoc = await this.updateDb(tab);
|
||||
if (doc.isDisabled === 1) {
|
||||
return newDoc;
|
||||
}
|
||||
if (this.isSixCron(doc)) {
|
||||
await cronClient.delCron([String(newDoc.id)]);
|
||||
}
|
||||
if (this.isSixCron(newDoc)) {
|
||||
await cronClient.addCron([
|
||||
{
|
||||
id: String(newDoc.id),
|
||||
schedule: newDoc.schedule!,
|
||||
command: newDoc.command,
|
||||
},
|
||||
]);
|
||||
}
|
||||
await this.set_crontab();
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
@@ -78,7 +103,8 @@ export default class CronService {
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
await CrontabModel.destroy({ where: { id: ids } });
|
||||
await this.set_crontab(true);
|
||||
await cronClient.delCron(ids.map(String));
|
||||
await this.set_crontab();
|
||||
}
|
||||
|
||||
public async pin(ids: number[]) {
|
||||
@@ -113,9 +139,80 @@ export default class CronService {
|
||||
}
|
||||
}
|
||||
|
||||
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
||||
let query = {};
|
||||
private formatViewQuery(query: any, viewQuery: any) {
|
||||
if (viewQuery.filters && viewQuery.filters.length > 0) {
|
||||
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
|
||||
if (!query[primaryOperate]) {
|
||||
query[primaryOperate] = [];
|
||||
}
|
||||
for (const col of viewQuery.filters) {
|
||||
const { property, value, operation } = col;
|
||||
let q: any = {};
|
||||
let operate2 = null;
|
||||
let operate = null;
|
||||
switch (operation) {
|
||||
case 'Reg':
|
||||
operate = Op.like;
|
||||
operate2 = Op.or;
|
||||
break;
|
||||
case 'NotReg':
|
||||
operate = Op.notLike;
|
||||
operate2 = Op.and;
|
||||
break;
|
||||
case 'In':
|
||||
q[Op.or] = [
|
||||
{
|
||||
[property]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
property === 'status' && value.includes(2)
|
||||
? { isDisabled: 1 }
|
||||
: {},
|
||||
];
|
||||
break;
|
||||
case 'Nin':
|
||||
q[Op.and] = [
|
||||
{
|
||||
[property]: {
|
||||
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
},
|
||||
property === 'status' && value.includes(2)
|
||||
? { isDisabled: { [Op.ne]: 1 } }
|
||||
: {},
|
||||
];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (operate && operate2) {
|
||||
q[property] = {
|
||||
[Op.or]: [
|
||||
{
|
||||
[operate2]: [
|
||||
{ [operate]: `%${value}%` },
|
||||
{ [operate]: `%${encodeURI(value)}%` },
|
||||
],
|
||||
},
|
||||
{
|
||||
[operate2]: [
|
||||
where(colFn(property), operate, `%${value}%`),
|
||||
where(colFn(property), operate, `%${encodeURI(value)}%`),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
query[primaryOperate].push(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatSearchText(query: any, searchText: string | undefined) {
|
||||
if (searchText) {
|
||||
if (!query[Op.and]) {
|
||||
query[Op.and] = [];
|
||||
}
|
||||
let q: any = {};
|
||||
const textArray = searchText.split(':');
|
||||
switch (textArray[0]) {
|
||||
case 'name':
|
||||
@@ -123,53 +220,136 @@ export default class CronService {
|
||||
case 'schedule':
|
||||
case 'label':
|
||||
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
|
||||
query = {
|
||||
[column]: {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${textArray[1]}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
||||
],
|
||||
},
|
||||
q[column] = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${textArray[1]}%` },
|
||||
{ [Op.like]: `%${encodeURI(textArray[1])}%` },
|
||||
],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||
],
|
||||
};
|
||||
query = {
|
||||
[Op.or]: [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
command: reg,
|
||||
},
|
||||
{
|
||||
schedule: reg,
|
||||
},
|
||||
{
|
||||
labels: reg,
|
||||
},
|
||||
{ [Op.like]: `%${encodeURI(searchText)}%` },
|
||||
],
|
||||
};
|
||||
q[Op.or] = [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
command: reg,
|
||||
},
|
||||
{
|
||||
schedule: reg,
|
||||
},
|
||||
{
|
||||
labels: reg,
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
query[Op.and].push(q);
|
||||
}
|
||||
}
|
||||
|
||||
private formatFilterQuery(query: any, filterQuery: any) {
|
||||
if (filterQuery) {
|
||||
if (!query[Op.and]) {
|
||||
query[Op.and] = [];
|
||||
}
|
||||
const filterKeys: any = Object.keys(filterQuery);
|
||||
for (const key of filterKeys) {
|
||||
let q: any = {};
|
||||
if (!filterQuery[key]) continue;
|
||||
if (key === 'status') {
|
||||
if (filterQuery[key].includes(CrontabStatus.disabled)) {
|
||||
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
|
||||
} else {
|
||||
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
|
||||
}
|
||||
} else {
|
||||
q[key] = filterQuery[key];
|
||||
}
|
||||
query[Op.and].push(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatViewSort(order: string[][], viewQuery: any) {
|
||||
if (viewQuery.sorts && viewQuery.sorts.length > 0) {
|
||||
for (const { property, type } of viewQuery.sorts) {
|
||||
order.unshift([property, type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async find({
|
||||
log_path,
|
||||
}: {
|
||||
log_path: string;
|
||||
}): Promise<Crontab | null> {
|
||||
try {
|
||||
const result = await CrontabModel.findAll({
|
||||
where: query,
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
return result as any;
|
||||
const result = await CrontabModel.findOne({ where: { log_path } });
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<Crontab> {
|
||||
public async crontabs(params?: {
|
||||
searchValue: string;
|
||||
page: string;
|
||||
size: string;
|
||||
sorter: string;
|
||||
filters: string;
|
||||
queryString: string;
|
||||
}): Promise<{ data: Crontab[]; total: number }> {
|
||||
const searchText = params?.searchValue;
|
||||
const page = Number(params?.page || '0');
|
||||
const size = Number(params?.size || '0');
|
||||
const viewQuery = safeJSONParse(params?.queryString);
|
||||
const filterQuery = safeJSONParse(params?.filters);
|
||||
const sorterQuery = safeJSONParse(params?.sorter);
|
||||
|
||||
let query: any = {};
|
||||
let order = [
|
||||
['isPinned', 'DESC'],
|
||||
['isDisabled', 'ASC'],
|
||||
['status', 'ASC'],
|
||||
['createdAt', 'DESC'],
|
||||
];
|
||||
|
||||
this.formatViewQuery(query, viewQuery);
|
||||
this.formatSearchText(query, searchText);
|
||||
this.formatFilterQuery(query, filterQuery);
|
||||
this.formatViewSort(order, viewQuery);
|
||||
|
||||
if (sorterQuery) {
|
||||
const { field, type } = sorterQuery;
|
||||
if (field && type) {
|
||||
order.unshift([field, type]);
|
||||
}
|
||||
}
|
||||
let condition: any = {
|
||||
where: query,
|
||||
order: order,
|
||||
};
|
||||
if (page && size) {
|
||||
condition.offset = (page - 1) * size;
|
||||
condition.limit = size;
|
||||
}
|
||||
try {
|
||||
const result = await CrontabModel.findAll(condition);
|
||||
const count = await CrontabModel.count({ where: query });
|
||||
return { data: result, total: count };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {
|
||||
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Crontab);
|
||||
}
|
||||
@@ -179,10 +359,9 @@ export default class CronService {
|
||||
{ status: CrontabStatus.queued },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
concurrentRun(
|
||||
ids.map((id) => async () => await this.runSingle(id)),
|
||||
10,
|
||||
);
|
||||
ids.forEach((id) => {
|
||||
this.runSingle(id);
|
||||
});
|
||||
}
|
||||
|
||||
public async stop(ids: number[]) {
|
||||
@@ -190,31 +369,11 @@ export default class CronService {
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
try {
|
||||
process.kill(-doc.pid);
|
||||
await killTask(doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.silly(error);
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
const err = await this.killTask(doc.command);
|
||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||
|
||||
const endTime = dayjs();
|
||||
const diffTimeStr = doc.last_execution_time
|
||||
? `,耗时 ${endTime.diff(
|
||||
dayjs(doc.last_execution_time * 1000),
|
||||
'second',
|
||||
)}`
|
||||
: '';
|
||||
if (logFileExist) {
|
||||
const str = err ? `\n${err}` : '';
|
||||
fs.appendFileSync(
|
||||
`${absolutePath}`,
|
||||
`${str}\n## 执行结束... ${endTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)}${diffTimeStr}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await CrontabModel.update(
|
||||
@@ -223,112 +382,90 @@ export default class CronService {
|
||||
);
|
||||
}
|
||||
|
||||
public async killTask(name: string) {
|
||||
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
||||
const execAsync = promisify(exec);
|
||||
try {
|
||||
let pid = (await execAsync(taskCommand)).stdout;
|
||||
if (pid) {
|
||||
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let pids = pid.match(/\(\d+/g);
|
||||
const killLogs = [];
|
||||
if (pids && pids.length > 0) {
|
||||
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
|
||||
pids = pids.slice(0, 3);
|
||||
for (const id of pids) {
|
||||
const c = `kill -9 ${id.slice(1)}`;
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(c);
|
||||
if (stderr) {
|
||||
killLogs.push(stderr);
|
||||
}
|
||||
if (stdout) {
|
||||
killLogs.push(stdout);
|
||||
}
|
||||
} catch (error: any) {
|
||||
killLogs.push(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
||||
} catch (e) {
|
||||
return JSON.stringify(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async runSingle(cronId: number): Promise<number> {
|
||||
return new Promise(async (resolve: any) => {
|
||||
const cron = await this.getDb({ id: cronId });
|
||||
if (cron.status !== CrontabStatus.queued) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
let { id, command, log_path } = cron;
|
||||
const absolutePath = path.resolve(config.logPath, `${log_path}`);
|
||||
const logFileExist = log_path && (await fileExist(absolutePath));
|
||||
|
||||
this.logger.silly('Running job');
|
||||
this.logger.silly('ID: ' + id);
|
||||
this.logger.silly('Original command: ' + command);
|
||||
|
||||
let cmdStr = command;
|
||||
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
||||
cmdStr = `task ${cmdStr}`;
|
||||
}
|
||||
if (cmdStr.endsWith('.js')) {
|
||||
cmdStr = `${cmdStr} now`;
|
||||
}
|
||||
|
||||
const cp = spawn(cmdStr, { shell: '/bin/bash' });
|
||||
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.running, pid: cp.pid },
|
||||
{ where: { id } },
|
||||
);
|
||||
cp.stderr.on('data', (data) => {
|
||||
if (logFileExist) {
|
||||
fs.appendFileSync(`${absolutePath}`, `${data}`);
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
return new Promise(async (resolve: any) => {
|
||||
const cron = await this.getDb({ id: cronId });
|
||||
if (cron.status !== CrontabStatus.queued) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
});
|
||||
cp.on('error', (err) => {
|
||||
if (logFileExist) {
|
||||
|
||||
let { id, command, log_path } = cron;
|
||||
const uniqPath = await getUniqPath(command, `${id}`);
|
||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||
if (log_path?.split('/')?.every(x => x !== uniqPath)) {
|
||||
fs.mkdirSync(logDirPath, { recursive: true });
|
||||
}
|
||||
const logPath = `${uniqPath}/${logTime}.log`;
|
||||
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
||||
|
||||
this.logger.silly('Running job');
|
||||
this.logger.silly('ID: ' + id);
|
||||
this.logger.silly('Original command: ' + command);
|
||||
|
||||
let cmdStr = command;
|
||||
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
|
||||
cmdStr = `${TASK_PREFIX}${cmdStr}`;
|
||||
}
|
||||
if (
|
||||
cmdStr.endsWith('.js') ||
|
||||
cmdStr.endsWith('.py') ||
|
||||
cmdStr.endsWith('.pyc') ||
|
||||
cmdStr.endsWith('.sh') ||
|
||||
cmdStr.endsWith('.ts')
|
||||
) {
|
||||
cmdStr = `${cmdStr} now`;
|
||||
}
|
||||
|
||||
const cp = spawn(`real_log_path=${logPath} ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
|
||||
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
|
||||
{ where: { id } },
|
||||
);
|
||||
cp.stderr.on('data', (data) => {
|
||||
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
|
||||
});
|
||||
cp.on('error', (err) => {
|
||||
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||
);
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
cp.on('close', async (code) => {
|
||||
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
resolve();
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`[panel][任务退出] 任务 ${command} 进程id: ${cp.pid}, 退出码 ${code}`,
|
||||
);
|
||||
});
|
||||
cp.on('close', async (code) => {
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||
await this.set_crontab(true);
|
||||
await cronClient.delCron(ids.map(String));
|
||||
await this.set_crontab();
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||
await this.set_crontab(true);
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
const sixCron = docs
|
||||
.filter((x) => this.isSixCron(x))
|
||||
.map((doc) => ({
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: doc.command,
|
||||
}));
|
||||
await cronClient.addCron(sixCron);
|
||||
await this.set_crontab();
|
||||
}
|
||||
|
||||
public async log(id: number) {
|
||||
@@ -341,69 +478,26 @@ export default class CronService {
|
||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||
if (logFileExist) {
|
||||
return getFileContentByName(`${absolutePath}`);
|
||||
}
|
||||
const [, commandStr, url] = doc.command.split(/ +/);
|
||||
let logPath = this.getKey(commandStr);
|
||||
const isQlCommand = doc.command.startsWith('ql ');
|
||||
const key =
|
||||
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
|
||||
logPath;
|
||||
if (isQlCommand) {
|
||||
logPath = 'update';
|
||||
}
|
||||
let logDir = `${config.logPath}${logPath}`;
|
||||
if (existsSync(logDir)) {
|
||||
let files = await promises.readdir(logDir);
|
||||
if (isQlCommand) {
|
||||
files = files.filter((x) => x.includes(key));
|
||||
}
|
||||
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
|
||||
} else {
|
||||
return '';
|
||||
return '任务未运行';
|
||||
}
|
||||
}
|
||||
|
||||
public async logs(id: number) {
|
||||
const doc = await this.getDb({ id });
|
||||
if (!doc) {
|
||||
if (!doc || !doc.log_path) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (doc.log_path) {
|
||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||
const dir = path.resolve(config.logPath, relativeDir);
|
||||
if (existsSync(dir)) {
|
||||
let files = await promises.readdir(dir);
|
||||
return files
|
||||
.map((x) => ({
|
||||
filename: x,
|
||||
directory: relativeDir.replace(config.logPath, ''),
|
||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
||||
}))
|
||||
.sort((a, b) => b.time - a.time);
|
||||
}
|
||||
}
|
||||
|
||||
const [, commandStr, url] = doc.command.split(/ +/);
|
||||
let logPath = this.getKey(commandStr);
|
||||
const isQlCommand = doc.command.startsWith('ql ');
|
||||
const key =
|
||||
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
|
||||
logPath;
|
||||
if (isQlCommand) {
|
||||
logPath = 'update';
|
||||
}
|
||||
let logDir = `${config.logPath}${logPath}`;
|
||||
if (existsSync(logDir)) {
|
||||
let files = await promises.readdir(logDir);
|
||||
if (isQlCommand) {
|
||||
files = files.filter((x) => x.includes(key));
|
||||
}
|
||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||
const dir = path.resolve(config.logPath, relativeDir);
|
||||
if (existsSync(dir)) {
|
||||
let files = await promises.readdir(dir);
|
||||
return files
|
||||
.map((x) => ({
|
||||
filename: x,
|
||||
directory: logPath,
|
||||
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(),
|
||||
directory: relativeDir.replace(config.logPath, ''),
|
||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
||||
}))
|
||||
.sort((a, b) => b.time - a.time);
|
||||
} else {
|
||||
@@ -411,37 +505,19 @@ export default class CronService {
|
||||
}
|
||||
}
|
||||
|
||||
private getKey(command: string): string {
|
||||
const start =
|
||||
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
||||
const end =
|
||||
command.lastIndexOf('.') !== -1
|
||||
? command.lastIndexOf('.')
|
||||
: command.length;
|
||||
|
||||
const tmpStr = command.substring(0, start - 1);
|
||||
let index = 0;
|
||||
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
|
||||
index = tmpStr.lastIndexOf('/');
|
||||
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
|
||||
index = tmpStr.lastIndexOf(':');
|
||||
}
|
||||
if (index) {
|
||||
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
|
||||
} else {
|
||||
return command.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
private make_command(tab: Crontab) {
|
||||
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
||||
let command = tab.command.trim();
|
||||
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||
command = `${TASK_PREFIX}${tab.command}`;
|
||||
}
|
||||
const crontab_job_string = `ID=${tab.id} ${command}`;
|
||||
return crontab_job_string;
|
||||
}
|
||||
|
||||
private async set_crontab(needReloadSchedule: boolean = false) {
|
||||
const tabs = await this.crontabs();
|
||||
private async set_crontab(data?: { data: Crontab[]; total: number }) {
|
||||
const tabs = data ?? (await this.crontabs());
|
||||
var crontab_string = '';
|
||||
tabs.forEach((tab) => {
|
||||
tabs.data.forEach((tab) => {
|
||||
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
|
||||
crontab_string += '# ';
|
||||
@@ -457,13 +533,9 @@ export default class CronService {
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.silly(crontab_string);
|
||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||
|
||||
execSync(`crontab ${config.crontabFile}`);
|
||||
if (needReloadSchedule) {
|
||||
exec(`pm2 reload schedule`);
|
||||
}
|
||||
await CrontabModel.update({ saved: true }, { where: {} });
|
||||
}
|
||||
|
||||
@@ -501,7 +573,17 @@ export default class CronService {
|
||||
});
|
||||
}
|
||||
|
||||
public autosave_crontab() {
|
||||
return this.set_crontab();
|
||||
public async autosave_crontab() {
|
||||
const tabs = await this.crontabs();
|
||||
this.set_crontab(tabs);
|
||||
|
||||
const sixCron = tabs.data
|
||||
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
|
||||
.map((doc) => ({
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: doc.command,
|
||||
}));
|
||||
await cronClient.addCron(sixCron);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
||||
import {
|
||||
initPosition,
|
||||
maxPosition,
|
||||
minPosition,
|
||||
stepPosition,
|
||||
} from '../data/env';
|
||||
import { FindOptions } from 'sequelize';
|
||||
|
||||
@Service()
|
||||
export default class CronViewService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public async create(payload: CrontabView): Promise<CrontabView> {
|
||||
let position = initPosition;
|
||||
const views = await this.list();
|
||||
if (views && views.length > 0 && views[views.length - 1].position) {
|
||||
position = views[views.length - 1].position as number;
|
||||
}
|
||||
position = position / 2;
|
||||
const tab = new CrontabView({ ...payload, position });
|
||||
const doc = await this.insert(tab);
|
||||
|
||||
await this.checkPosition(tab.position!);
|
||||
return doc;
|
||||
}
|
||||
|
||||
public async insert(payload: CrontabView): Promise<CrontabView> {
|
||||
return await CrontabViewModel.create(payload, { returning: true });
|
||||
}
|
||||
|
||||
public async update(payload: CrontabView): Promise<CrontabView> {
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new CrontabView({ ...doc, ...payload });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
public async updateDb(payload: CrontabView): Promise<CrontabView> {
|
||||
await CrontabViewModel.update(payload, { where: { id: payload.id } });
|
||||
return await this.getDb({ id: payload.id });
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
await CrontabViewModel.destroy({ where: { id: ids } });
|
||||
}
|
||||
|
||||
public async list(): Promise<CrontabView[]> {
|
||||
try {
|
||||
const result = await CrontabViewModel.findAll({
|
||||
where: {},
|
||||
order: [['position', 'DESC']],
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getDb(
|
||||
query: FindOptions<CrontabView>['where'],
|
||||
): Promise<CrontabView> {
|
||||
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as CrontabView);
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
private async checkPosition(position: number) {
|
||||
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
|
||||
const envs = await this.list();
|
||||
let position = initPosition;
|
||||
for (const env of envs) {
|
||||
position = position - stepPosition;
|
||||
await this.updateDb({ id: env.id, position });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPrecisionPosition(position: number): number {
|
||||
return parseFloat(position.toPrecision(16));
|
||||
}
|
||||
|
||||
public async move({
|
||||
id,
|
||||
fromIndex,
|
||||
toIndex,
|
||||
}: {
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
id: number;
|
||||
}): Promise<CrontabView> {
|
||||
let targetPosition: number;
|
||||
const isUpward = fromIndex > toIndex;
|
||||
const views = await this.list();
|
||||
if (toIndex === 0 || toIndex === views.length - 1) {
|
||||
targetPosition = isUpward
|
||||
? views[0].position! * 2
|
||||
: views[toIndex].position! / 2;
|
||||
} else {
|
||||
targetPosition = isUpward
|
||||
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
|
||||
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
|
||||
}
|
||||
const newDoc = await this.update({
|
||||
id,
|
||||
position: this.getPrecisionPosition(targetPosition),
|
||||
});
|
||||
|
||||
await this.checkPosition(targetPosition);
|
||||
return newDoc;
|
||||
}
|
||||
}
|
||||
+174
-104
@@ -8,13 +8,15 @@ import {
|
||||
DependenceTypes,
|
||||
unInstallDependenceCommandTypes,
|
||||
DependenceModel,
|
||||
GetDependenceCommandTypes,
|
||||
versionDependenceCommandTypes,
|
||||
} from '../data/dependence';
|
||||
import _ from 'lodash';
|
||||
import { spawn } from 'child_process';
|
||||
import { spawn } from 'cross-spawn';
|
||||
import SockService from './sock';
|
||||
import { Op } from 'sequelize';
|
||||
import { concurrentRun } from '../config/util';
|
||||
import { FindOptions, Op } from 'sequelize';
|
||||
import { promiseExecSuccess } from '../config/util';
|
||||
import dayjs from 'dayjs';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
|
||||
@Service()
|
||||
export default class DependenceService {
|
||||
@@ -25,7 +27,7 @@ export default class DependenceService {
|
||||
|
||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||
const tabs = payloads.map((x) => {
|
||||
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
|
||||
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
|
||||
return tab;
|
||||
});
|
||||
const docs = await this.insert(tabs);
|
||||
@@ -46,7 +48,7 @@ export default class DependenceService {
|
||||
const tab = new Dependence({
|
||||
...doc,
|
||||
...other,
|
||||
status: DependenceStatus.installing,
|
||||
status: DependenceStatus.queued,
|
||||
});
|
||||
const newDoc = await this.updateDb(tab);
|
||||
this.installDependenceOneByOne([newDoc]);
|
||||
@@ -60,7 +62,7 @@ export default class DependenceService {
|
||||
|
||||
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.removing, log: [] },
|
||||
{ status: DependenceStatus.queued, log: [] },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
@@ -79,7 +81,7 @@ export default class DependenceService {
|
||||
): Promise<Dependence[]> {
|
||||
let condition = { ...query, type: DependenceTypes[type as any] };
|
||||
if (searchValue) {
|
||||
const encodeText = encodeURIComponent(searchValue);
|
||||
const encodeText = encodeURI(searchValue);
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchValue}%` },
|
||||
@@ -100,23 +102,19 @@ export default class DependenceService {
|
||||
}
|
||||
}
|
||||
|
||||
private installDependenceOneByOne(
|
||||
public installDependenceOneByOne(
|
||||
docs: Dependence[],
|
||||
isInstall: boolean = true,
|
||||
force: boolean = false,
|
||||
) {
|
||||
concurrentRun(
|
||||
docs.map(
|
||||
(dep) => async () =>
|
||||
await this.installOrUninstallDependencies([dep], isInstall, force),
|
||||
),
|
||||
1,
|
||||
);
|
||||
docs.forEach((dep) => {
|
||||
this.installOrUninstallDependency(dep, isInstall, force);
|
||||
});
|
||||
}
|
||||
|
||||
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.installing, log: [] },
|
||||
{ status: DependenceStatus.queued, log: [] },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
|
||||
@@ -125,118 +123,190 @@ export default class DependenceService {
|
||||
return docs;
|
||||
}
|
||||
|
||||
private async find(query: any, sort?: any): Promise<Dependence[]> {
|
||||
const docs = await DependenceModel.findAll({ where: { ...query } });
|
||||
private async find(query: any, sort: any = []): Promise<Dependence[]> {
|
||||
const docs = await DependenceModel.findAll({
|
||||
where: { ...query },
|
||||
order: [...sort, ['createdAt', 'DESC']],
|
||||
});
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<Dependence> {
|
||||
public async getDb(
|
||||
query: FindOptions<Dependence>['where'],
|
||||
): Promise<Dependence> {
|
||||
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Dependence);
|
||||
}
|
||||
|
||||
private async updateLog(ids: number[], log: string): Promise<void> {
|
||||
const doc = await DependenceModel.findOne({ where: { id: ids } });
|
||||
const newLog = doc?.log ? [...doc.log, log] : [log];
|
||||
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
|
||||
taskLimit.updateDepLog(async () => {
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
const newLog = doc?.log ? [...doc.log, log] : [log];
|
||||
await DependenceModel.update(
|
||||
{ log: newLog },
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public installOrUninstallDependencies(
|
||||
dependencies: Dependence[],
|
||||
public installOrUninstallDependency(
|
||||
dependency: Dependence,
|
||||
isInstall: boolean = true,
|
||||
force: boolean = false,
|
||||
) {
|
||||
return new Promise(async (resolve) => {
|
||||
if (dependencies.length === 0) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const socketMessageType = !force
|
||||
? 'installDependence'
|
||||
: 'uninstallDependence';
|
||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
||||
const depRunCommand = (
|
||||
isInstall
|
||||
? InstallDependenceCommandTypes
|
||||
: unInstallDependenceCommandTypes
|
||||
)[dependencies[0].type as any];
|
||||
const actionText = isInstall ? '安装' : '删除';
|
||||
const depIds = dependencies.map((x) => x.id) as number[];
|
||||
const startTime = dayjs();
|
||||
return taskLimit.runOneByOne(() => {
|
||||
return new Promise(async (resolve) => {
|
||||
const depIds = [dependency.id!];
|
||||
const status = isInstall
|
||||
? DependenceStatus.installing
|
||||
: DependenceStatus.removing;
|
||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||
|
||||
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)}\n\n`;
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message,
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, message);
|
||||
const socketMessageType = isInstall
|
||||
? 'installDependence'
|
||||
: 'uninstallDependence';
|
||||
let depName = dependency.name.trim();
|
||||
const depRunCommand = (
|
||||
isInstall
|
||||
? InstallDependenceCommandTypes
|
||||
: unInstallDependenceCommandTypes
|
||||
)[dependency.type];
|
||||
const actionText = isInstall ? '安装' : '删除';
|
||||
const startTime = dayjs();
|
||||
|
||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||
|
||||
cp.stdout.on('data', async (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.stderr.on('data', async (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.on('error', async (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message: JSON.stringify(err),
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, JSON.stringify(err));
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
const endTime = dayjs();
|
||||
const isSucceed = code === 0;
|
||||
const resultText = isSucceed ? '成功' : '失败';
|
||||
|
||||
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
|
||||
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||
)}\n\n`;
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message,
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, message);
|
||||
this.updateLog(depIds, message);
|
||||
|
||||
let status = null;
|
||||
if (isSucceed) {
|
||||
status = isInstall
|
||||
? DependenceStatus.installed
|
||||
: DependenceStatus.removed;
|
||||
} else {
|
||||
status = isInstall
|
||||
? DependenceStatus.installFailed
|
||||
: DependenceStatus.removeFailed;
|
||||
}
|
||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||
// 判断是否已经安装过依赖
|
||||
if (isInstall) {
|
||||
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
|
||||
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
||||
let depVersion = '';
|
||||
if (depName.includes(depVersionStr)) {
|
||||
const symbolRegx = new RegExp(
|
||||
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
|
||||
);
|
||||
const [, _depName, _depVersion] = depName.match(symbolRegx) || [];
|
||||
if (_depVersion && _depName) {
|
||||
depName = _depName;
|
||||
depVersion = _depVersion;
|
||||
}
|
||||
}
|
||||
const isNodeDependence = dependency.type === DependenceTypes.nodejs;
|
||||
const isLinuxDependence = dependency.type === DependenceTypes.linux;
|
||||
const isPythonDependence =
|
||||
dependency.type === DependenceTypes.python3;
|
||||
const depInfo = (
|
||||
await promiseExecSuccess(
|
||||
isNodeDependence
|
||||
? `${getCommandPrefix} | grep "${depName}" | head -1`
|
||||
: `${getCommandPrefix} ${depName}`,
|
||||
)
|
||||
)
|
||||
.replace(/\s{2,}/, ' ')
|
||||
.replace(/\s+$/, '');
|
||||
|
||||
// 如果删除依赖成功或者强制删除
|
||||
if ((isSucceed || force) && !isInstall) {
|
||||
this.removeDb(depIds);
|
||||
if (
|
||||
depInfo &&
|
||||
((isNodeDependence && depInfo.split(' ')?.[0] === depName) ||
|
||||
(isLinuxDependence &&
|
||||
depInfo.toLocaleLowerCase().includes('installed')) ||
|
||||
isPythonDependence) &&
|
||||
(!depVersion || depInfo.includes(depVersion))
|
||||
) {
|
||||
const endTime = dayjs();
|
||||
const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message: _message,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, _message);
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.installed },
|
||||
{ where: { id: depIds } },
|
||||
);
|
||||
return resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
const cp = spawn(`${depRunCommand} ${dependency.name.trim()}`, {
|
||||
shell: '/bin/bash',
|
||||
});
|
||||
|
||||
cp.stdout.on('data', async (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.stderr.on('data', async (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.on('error', async (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message: JSON.stringify(err),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
const endTime = dayjs();
|
||||
const isSucceed = code === 0;
|
||||
const resultText = isSucceed ? '成功' : '失败';
|
||||
|
||||
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, message);
|
||||
|
||||
let status = null;
|
||||
if (isSucceed) {
|
||||
status = isInstall
|
||||
? DependenceStatus.installed
|
||||
: DependenceStatus.removed;
|
||||
} else {
|
||||
status = isInstall
|
||||
? DependenceStatus.installFailed
|
||||
: DependenceStatus.removeFailed;
|
||||
}
|
||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||
|
||||
// 如果删除依赖成功或者强制删除
|
||||
if ((isSucceed || force) && !isInstall) {
|
||||
this.removeDb(depIds);
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+68
-36
@@ -1,11 +1,18 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
|
||||
import _ from 'lodash';
|
||||
import { Op } from 'sequelize';
|
||||
import {
|
||||
Env,
|
||||
EnvModel,
|
||||
EnvStatus,
|
||||
initPosition,
|
||||
maxPosition,
|
||||
minPosition,
|
||||
stepPosition,
|
||||
} from '../data/env';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { FindOptions, Op } from 'sequelize';
|
||||
|
||||
@Service()
|
||||
export default class EnvService {
|
||||
@@ -13,17 +20,22 @@ export default class EnvService {
|
||||
|
||||
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;
|
||||
let position = initPosition;
|
||||
if (
|
||||
envs &&
|
||||
envs.length > 0 &&
|
||||
typeof envs[envs.length - 1].position === 'number'
|
||||
) {
|
||||
position = envs[envs.length - 1].position!;
|
||||
}
|
||||
const tabs = payloads.map((x) => {
|
||||
position = position / 2;
|
||||
position = position - stepPosition;
|
||||
const tab = new Env({ ...x, position });
|
||||
return tab;
|
||||
});
|
||||
const docs = await this.insert(tabs);
|
||||
await this.set_envs();
|
||||
await this.checkPosition(tabs[tabs.length - 1].position!);
|
||||
return docs;
|
||||
}
|
||||
|
||||
@@ -37,7 +49,9 @@ export default class EnvService {
|
||||
}
|
||||
|
||||
public async update(payload: Env): Promise<Env> {
|
||||
const newDoc = await this.updateDb(payload);
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new Env({ ...doc, ...payload });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
await this.set_envs();
|
||||
return newDoc;
|
||||
}
|
||||
@@ -67,28 +81,47 @@ export default class EnvService {
|
||||
const envs = await this.envs();
|
||||
if (toIndex === 0 || toIndex === envs.length - 1) {
|
||||
targetPosition = isUpward
|
||||
? envs[0].position * 2
|
||||
: envs[toIndex].position / 2;
|
||||
? envs[0].position! + stepPosition
|
||||
: envs[toIndex].position! - stepPosition;
|
||||
} else {
|
||||
targetPosition = isUpward
|
||||
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
|
||||
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
||||
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
|
||||
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
|
||||
}
|
||||
|
||||
const newDoc = await this.update({
|
||||
id,
|
||||
position: targetPosition,
|
||||
position: this.getPrecisionPosition(targetPosition),
|
||||
});
|
||||
|
||||
await this.checkPosition(targetPosition, envs[toIndex].position!);
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
public async envs(
|
||||
searchText: string = '',
|
||||
sort: any = { position: -1 },
|
||||
query: any = {},
|
||||
): Promise<Env[]> {
|
||||
private async checkPosition(position: number, edge: number = 0) {
|
||||
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||
if (
|
||||
precisionPosition < minPosition ||
|
||||
precisionPosition > maxPosition ||
|
||||
Math.abs(precisionPosition - edge) < minPosition
|
||||
) {
|
||||
const envs = await this.envs();
|
||||
let position = initPosition;
|
||||
for (const env of envs) {
|
||||
position = position - stepPosition;
|
||||
await this.updateDb({ id: env.id, position });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPrecisionPosition(position: number): number {
|
||||
return parseFloat(position.toPrecision(16));
|
||||
}
|
||||
|
||||
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const encodeText = encodeURIComponent(searchText);
|
||||
const encodeText = encodeURI(searchText);
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
@@ -112,7 +145,10 @@ export default class EnvService {
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await this.find(condition, [['position', 'DESC']]);
|
||||
const result = await this.find(condition, [
|
||||
['position', 'DESC'],
|
||||
['createdAt', 'ASC'],
|
||||
]);
|
||||
return result as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -127,7 +163,7 @@ export default class EnvService {
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<Env> {
|
||||
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
||||
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Env);
|
||||
}
|
||||
@@ -151,28 +187,24 @@ export default class EnvService {
|
||||
}
|
||||
|
||||
public async set_envs() {
|
||||
const envs = await this.envs(
|
||||
'',
|
||||
{ position: -1 },
|
||||
{ name: { [Op.not]: null } },
|
||||
);
|
||||
const groups = _.groupBy(envs, 'name');
|
||||
const envs = await this.envs('', {
|
||||
name: { [Op.not]: null },
|
||||
status: EnvStatus.normal,
|
||||
});
|
||||
const groups = groupBy(envs, 'name');
|
||||
let env_string = '';
|
||||
for (const key in groups) {
|
||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||
const group = groups[key];
|
||||
|
||||
// 忽略不符合bash要求的环境变量名称
|
||||
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
||||
let value = _(group)
|
||||
.filter((x) => x.status !== EnvStatus.disabled)
|
||||
.map('value')
|
||||
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
|
||||
let value = group
|
||||
.map((x) => x.value)
|
||||
.join('&')
|
||||
.replace(/(\\)[^n]/g, '\\\\')
|
||||
.replace(/(\\$)/, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/'/g, "'\\''")
|
||||
.trim();
|
||||
env_string += `export ${key}="${value}"\n`;
|
||||
env_string += `export ${key}='${value}'\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+438
-153
@@ -6,6 +6,7 @@ import got from 'got';
|
||||
import nodemailer from 'nodemailer';
|
||||
import crypto from 'crypto';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
import { parseBody, parseHeaders } from '../config/util';
|
||||
|
||||
@Service()
|
||||
export default class NotificationService {
|
||||
@@ -17,20 +18,28 @@ export default class NotificationService {
|
||||
['goCqHttpBot', this.goCqHttpBot],
|
||||
['serverChan', this.serverChan],
|
||||
['pushDeer', this.pushDeer],
|
||||
['chat', this.chat],
|
||||
['bark', this.bark],
|
||||
['telegramBot', this.telegramBot],
|
||||
['dingtalkBot', this.dingtalkBot],
|
||||
['weWorkBot', this.weWorkBot],
|
||||
['weWorkApp', this.weWorkApp],
|
||||
['aibotk', this.aibotk],
|
||||
['iGot', this.iGot],
|
||||
['pushPlus', this.pushPlus],
|
||||
['email', this.email],
|
||||
['pushMe', this.pushMe],
|
||||
['webhook', this.webhook],
|
||||
['lark', this.lark],
|
||||
]);
|
||||
|
||||
private timeout = 10000;
|
||||
private title = '';
|
||||
private content = '';
|
||||
private params!: Omit<NotificationInfo, 'type'>;
|
||||
private gotOption = {
|
||||
timeout: 10000,
|
||||
retry: 1,
|
||||
};
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
@@ -70,35 +79,49 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async gotify() {
|
||||
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
||||
const res: any = await got
|
||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `title=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&message=${encodeURIComponent(
|
||||
this.content,
|
||||
)}&priority=${gotifyPriority}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return typeof res.id === 'number';
|
||||
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||
...this.gotOption,
|
||||
body: `title=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&message=${encodeURIComponent(
|
||||
this.content,
|
||||
)}&priority=${gotifyPriority}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
if (typeof res.id === 'number') {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async goCqHttpBot() {
|
||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||
const res: any = await got
|
||||
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: { message: `${this.title}\n${this.content}` },
|
||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||
})
|
||||
.json();
|
||||
return res.retcode === 0;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||
...this.gotOption,
|
||||
json: { message: `${this.title}\n${this.content}` },
|
||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||
})
|
||||
.json();
|
||||
if (res.retcode === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async serverChan() {
|
||||
@@ -106,53 +129,97 @@ export default class NotificationService {
|
||||
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;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
body: `title=${this.title}&desp=${this.content}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
if (res.errno === 0 || res.data.errno === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async pushDeer() {
|
||||
const { pushDeerKey } = this.params;
|
||||
const url = `https://api2.pushdeer.com/message/push`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
return (
|
||||
res.content.result.length !== undefined && res.content.result.length > 0
|
||||
);
|
||||
const { pushDeerKey, pushDeerUrl } = this.params;
|
||||
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
if (
|
||||
res.content.result.length !== undefined &&
|
||||
res.content.result.length > 0
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async chat() {
|
||||
const { chatUrl, chatToken } = this.params;
|
||||
const url = `${chatUrl}${chatToken}`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
if (res.success) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async bark() {
|
||||
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
||||
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
||||
if (!barkPush.startsWith('http')) {
|
||||
barkPush = `https://api.day.app/${barkPush}`;
|
||||
}
|
||||
const url = `${barkPush}/${encodeURIComponent(
|
||||
this.title,
|
||||
)}/${encodeURIComponent(
|
||||
this.content,
|
||||
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
|
||||
const res: any = await got
|
||||
.get(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
return res.code === 200;
|
||||
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
|
||||
|
||||
try {
|
||||
const res: any = await got
|
||||
.get(url, {
|
||||
...this.gotOption,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
if (res.code === 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async telegramBot() {
|
||||
@@ -184,16 +251,23 @@ export default class NotificationService {
|
||||
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;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
agent,
|
||||
})
|
||||
.json();
|
||||
if (res.ok) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async dingtalkBot() {
|
||||
@@ -207,48 +281,63 @@ export default class NotificationService {
|
||||
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}`,
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: ` ${this.title}\n\n${this.content}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return res.errcode === 0;
|
||||
})
|
||||
.json();
|
||||
if (res.errcode === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async 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}`,
|
||||
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
||||
this.params;
|
||||
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: ` ${this.title}\n\n${this.content}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return res.errcode === 0;
|
||||
})
|
||||
.json();
|
||||
if (res.errcode === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async weWorkApp() {
|
||||
const { weWorkAppKey } = this.params;
|
||||
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
||||
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
|
||||
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
|
||||
const tokenRes: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
...this.gotOption,
|
||||
json: {
|
||||
corpid,
|
||||
corpsecret,
|
||||
@@ -294,78 +383,274 @@ export default class NotificationService {
|
||||
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,
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(
|
||||
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
||||
{
|
||||
...this.gotOption,
|
||||
json: {
|
||||
touser,
|
||||
agentid,
|
||||
safe: '0',
|
||||
...options,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
.json();
|
||||
)
|
||||
.json();
|
||||
|
||||
return res.errcode === 0;
|
||||
if (res.errcode === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async aibotk() {
|
||||
const { aibotkKey, aibotkType, aibotkName } = this.params;
|
||||
let url = '';
|
||||
let json = {};
|
||||
switch (aibotkType) {
|
||||
case 'room':
|
||||
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
|
||||
json = {
|
||||
apiKey: `${aibotkKey}`,
|
||||
roomName: `${aibotkName}`,
|
||||
message: {
|
||||
type: 1,
|
||||
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'contact':
|
||||
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
|
||||
json = {
|
||||
apiKey: `${aibotkKey}`,
|
||||
name: `${aibotkName}`,
|
||||
message: {
|
||||
type: 1,
|
||||
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
...json,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
if (res.code === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
body: `title=${this.title}&content=${this.content}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
|
||||
return res.ret === 0;
|
||||
if (res.ret === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
token: `${pushPlusToken}`,
|
||||
title: `${this.title}`,
|
||||
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||
topic: `${pushPlusUser || ''}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
return res.code === 200;
|
||||
if (res.code === 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async lark() {
|
||||
let { larkKey } = this.params;
|
||||
|
||||
if (!larkKey.startsWith('http')) {
|
||||
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(larkKey, {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
msg_type: 'text',
|
||||
content: { text: `${this.title}\n\n${this.content}` },
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
.json();
|
||||
if (res.StatusCode === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
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/>')}`,
|
||||
});
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: emailService,
|
||||
auth: {
|
||||
user: emailUser,
|
||||
pass: emailPass,
|
||||
},
|
||||
});
|
||||
|
||||
transporter.close();
|
||||
const info = await transporter.sendMail({
|
||||
from: `"青龙快讯" <${emailUser}>`,
|
||||
to: `${emailUser}`,
|
||||
subject: `${this.title}`,
|
||||
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||
});
|
||||
|
||||
return !!info.messageId;
|
||||
transporter.close();
|
||||
|
||||
if (info.messageId) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(info));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async pushMe() {
|
||||
const { pushMeKey } = this.params;
|
||||
try {
|
||||
const res: any = await got.post(
|
||||
`https://push.i-i.me/?push_key=${pushMeKey}`,
|
||||
{
|
||||
...this.gotOption,
|
||||
json: {
|
||||
title: this.title,
|
||||
content: this.content,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
);
|
||||
if (res.body === 'success') {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(res.body);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async webhook() {
|
||||
const {
|
||||
webhookUrl,
|
||||
webhookBody,
|
||||
webhookHeaders,
|
||||
webhookMethod,
|
||||
webhookContentType,
|
||||
} = this.params;
|
||||
|
||||
const { formatBody, formatUrl } = this.formatNotifyContent(
|
||||
webhookUrl,
|
||||
webhookBody,
|
||||
);
|
||||
if (!formatUrl && !formatBody) {
|
||||
return false;
|
||||
}
|
||||
const headers = parseHeaders(webhookHeaders);
|
||||
const body = parseBody(formatBody, webhookContentType);
|
||||
const bodyParam = this.formatBody(webhookContentType, body);
|
||||
const options = {
|
||||
method: webhookMethod,
|
||||
headers,
|
||||
...this.gotOption,
|
||||
allowGetBody: true,
|
||||
...bodyParam,
|
||||
};
|
||||
try {
|
||||
const res = await got(formatUrl, options);
|
||||
if (String(res.statusCode).startsWith('20')) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private formatBody(contentType: string, body: any): object {
|
||||
if (!body) return {};
|
||||
switch (contentType) {
|
||||
case 'application/json':
|
||||
return { json: body };
|
||||
case 'multipart/form-data':
|
||||
return { form: body };
|
||||
case 'application/x-www-form-urlencoded':
|
||||
return { body };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private formatNotifyContent(url: string, body: string) {
|
||||
if (!url.includes('$title') && !body.includes('$title')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
formatUrl: url
|
||||
?.replaceAll('$title', encodeURIComponent(this.title))
|
||||
?.replaceAll('$content', encodeURIComponent(this.content)),
|
||||
formatBody: body
|
||||
?.replaceAll('$title', this.title)
|
||||
?.replaceAll('$content', this.content),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export default class OpenService {
|
||||
): Promise<App[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const encodeText = encodeURIComponent(searchText);
|
||||
const encodeText = encodeURI(searchText);
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
@@ -119,7 +119,7 @@ export default class OpenService {
|
||||
(x) => x.expiration >= timestamp,
|
||||
);
|
||||
let tokens = invalidTokens;
|
||||
if (invalidTokens.length > 5) {
|
||||
if (invalidTokens.length >= 5) {
|
||||
tokens = [
|
||||
...invalidTokens.slice(0, 4),
|
||||
{ ...invalidTokens[4], expiration },
|
||||
|
||||
+74
-71
@@ -1,14 +1,16 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
|
||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||
import {
|
||||
ToadScheduler,
|
||||
LongIntervalJob,
|
||||
AsyncTask,
|
||||
SimpleIntervalSchedule,
|
||||
Task,
|
||||
} from 'toad-scheduler';
|
||||
import dayjs from 'dayjs';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import { spawn } from 'cross-spawn';
|
||||
|
||||
interface ScheduleTaskType {
|
||||
id: number;
|
||||
@@ -42,67 +44,68 @@ export default class ScheduleService {
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
async runTask(command: string, callbacks: TaskCallbacks = {}) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const startTime = dayjs();
|
||||
await callbacks.onBefore?.(startTime);
|
||||
async runTask(
|
||||
command: string,
|
||||
callbacks: TaskCallbacks = {},
|
||||
completionTime: 'start' | 'end' = 'end',
|
||||
) {
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const startTime = dayjs();
|
||||
await callbacks.onBefore?.(startTime);
|
||||
|
||||
const cp = spawn(command, { shell: '/bin/bash' });
|
||||
const cp = spawn(command, { shell: '/bin/bash' });
|
||||
|
||||
// TODO:
|
||||
callbacks.onStart?.(cp, startTime);
|
||||
callbacks.onStart?.(cp, startTime);
|
||||
completionTime === 'start' && resolve(cp.pid);
|
||||
|
||||
cp.stdout.on('data', async (data) => {
|
||||
await callbacks.onLog?.(data.toString());
|
||||
});
|
||||
cp.stdout.on('data', async (data) => {
|
||||
await callbacks.onLog?.(data.toString());
|
||||
});
|
||||
|
||||
cp.stderr.on('data', async (data) => {
|
||||
cp.stderr.on('data', async (data) => {
|
||||
this.logger.info(
|
||||
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||
command,
|
||||
data.toString(),
|
||||
);
|
||||
await callbacks.onError?.(data.toString());
|
||||
});
|
||||
|
||||
cp.on('error', async (err) => {
|
||||
this.logger.error(
|
||||
'[panel][创建任务失败] 命令: %s, 错误信息: %j',
|
||||
command,
|
||||
err,
|
||||
);
|
||||
await callbacks.onError?.(JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`[panel][任务退出] ${command} 进程id: ${cp.pid}, 退出码 ${code}`,
|
||||
);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
const endTime = dayjs();
|
||||
await callbacks.onEnd?.(
|
||||
cp,
|
||||
endTime,
|
||||
endTime.diff(startTime, 'seconds'),
|
||||
);
|
||||
resolve(null);
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
'执行任务 %s 失败,时间:%s, 错误信息:%j',
|
||||
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
data.toString(),
|
||||
error,
|
||||
);
|
||||
await callbacks.onError?.(data.toString());
|
||||
});
|
||||
|
||||
cp.on('error', async (err) => {
|
||||
this.logger.error(
|
||||
'创建任务 %s 失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
err,
|
||||
);
|
||||
await callbacks.onError?.(JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||
);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
const endTime = dayjs();
|
||||
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||
await callbacks.onEnd?.(
|
||||
cp,
|
||||
endTime,
|
||||
endTime.diff(startTime, 'seconds'),
|
||||
);
|
||||
resolve(null);
|
||||
});
|
||||
} catch (error) {
|
||||
await this.logger.error(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
error,
|
||||
);
|
||||
await callbacks.onError?.(JSON.stringify(error));
|
||||
resolve(null);
|
||||
}
|
||||
await callbacks.onError?.(JSON.stringify(error));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -113,7 +116,7 @@ export default class ScheduleService {
|
||||
) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info(
|
||||
'[创建cron任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
||||
'[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
|
||||
_id,
|
||||
schedule,
|
||||
name,
|
||||
@@ -123,19 +126,22 @@ export default class ScheduleService {
|
||||
this.scheduleStacks.set(
|
||||
_id,
|
||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||
await this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks);
|
||||
}),
|
||||
);
|
||||
|
||||
if (runImmediately) {
|
||||
await this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
||||
this.logger.info('[panel][取消定时任务], 任务名: %s', name);
|
||||
if (this.scheduleStacks.has(_id)) {
|
||||
this.scheduleStacks.get(_id)?.cancel();
|
||||
this.scheduleStacks.delete(_id);
|
||||
}
|
||||
}
|
||||
|
||||
async createIntervalTask(
|
||||
@@ -146,23 +152,20 @@ export default class ScheduleService {
|
||||
) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info(
|
||||
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
|
||||
'[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s',
|
||||
_id,
|
||||
name,
|
||||
command,
|
||||
);
|
||||
const task = new AsyncTask(
|
||||
const task = new Task(
|
||||
name,
|
||||
async () => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
await this.runTask(command, callbacks);
|
||||
});
|
||||
() => {
|
||||
this.runTask(command, callbacks);
|
||||
},
|
||||
(err) => {
|
||||
this.logger.error(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
'[执行任务失败] 命令: %s, 错误信息: %j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
err,
|
||||
);
|
||||
},
|
||||
@@ -177,13 +180,13 @@ export default class ScheduleService {
|
||||
this.intervalSchedule.addIntervalJob(job);
|
||||
|
||||
if (runImmediately) {
|
||||
await this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
|
||||
this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name);
|
||||
this.intervalSchedule.removeById(_id);
|
||||
}
|
||||
|
||||
|
||||
+17
-14
@@ -6,6 +6,8 @@ import SockService from './sock';
|
||||
import CronService from './cron';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import config from '../config';
|
||||
import { TASK_COMMAND } from '../config/const';
|
||||
import { getPid, killTask } from '../config/util';
|
||||
|
||||
@Service()
|
||||
export default class ScriptService {
|
||||
@@ -40,23 +42,24 @@ export default class ScriptService {
|
||||
|
||||
public async runScript(filePath: string) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
const command = `task -l ${relativePath} now`;
|
||||
this.scheduleService.runTask(command, this.taskCallbacks(filePath));
|
||||
const command = `${TASK_COMMAND} -l ${relativePath} now`;
|
||||
const pid = await this.scheduleService.runTask(
|
||||
command,
|
||||
this.taskCallbacks(filePath),
|
||||
'start',
|
||||
);
|
||||
|
||||
return { code: 200 };
|
||||
return { code: 200, data: pid };
|
||||
}
|
||||
|
||||
public async stopScript(filePath: string) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
|
||||
|
||||
const str = err ? `\n${err}` : '';
|
||||
this.sockService.sendMessage({
|
||||
type: 'manuallyRunScript',
|
||||
message: `${str}\n## 执行结束... ${new Date()
|
||||
.toLocaleString('zh', { hour12: false })
|
||||
.replace(' 24:', ' 00:')} `,
|
||||
});
|
||||
public async stopScript(filePath: string, pid: number) {
|
||||
if (!pid) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`) as number;
|
||||
}
|
||||
try {
|
||||
await killTask(pid);
|
||||
} catch (error) {}
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
+73
-41
@@ -1,20 +1,42 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import fs from 'fs';
|
||||
import fs, { existsSync } from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { Subscription } from '../data/subscription';
|
||||
import { formatUrl } from '../config/subscription';
|
||||
import config from '../config';
|
||||
|
||||
@Service()
|
||||
export default class SshKeyService {
|
||||
private homedir = os.homedir();
|
||||
private sshPath = path.resolve(this.homedir, '.ssh');
|
||||
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
|
||||
private sshPath = config.sshdPath;
|
||||
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');
|
||||
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
||||
this.initSshConfigFile();
|
||||
}
|
||||
|
||||
private initSshConfigFile() {
|
||||
let config = '';
|
||||
if (existsSync(this.sshConfigFilePath)) {
|
||||
config = fs.readFileSync(this.sshConfigFilePath, { encoding: 'utf-8' });
|
||||
} else {
|
||||
fs.writeFileSync(this.sshConfigFilePath, '');
|
||||
}
|
||||
if (!config.includes(this.sshConfigHeader)) {
|
||||
fs.writeFileSync(
|
||||
this.sshConfigFilePath,
|
||||
`${this.sshConfigHeader}\n\n${config}`,
|
||||
{ encoding: 'utf-8' },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private generatePrivateKeyFile(alias: string, key: string): void {
|
||||
try {
|
||||
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
|
||||
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
|
||||
encoding: 'utf8',
|
||||
mode: '400',
|
||||
});
|
||||
@@ -23,62 +45,72 @@ export default class SshKeyService {
|
||||
}
|
||||
}
|
||||
|
||||
private getConfigRegx(alias: string) {
|
||||
return new RegExp(
|
||||
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
|
||||
'g',
|
||||
);
|
||||
}
|
||||
|
||||
private removePrivateKeyFile(alias: string): void {
|
||||
try {
|
||||
fs.unlinkSync(`${this.sshPath}/${alias}`);
|
||||
const filePath = path.join(this.sshPath, alias);
|
||||
if (existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('删除私钥文件失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
private generateSingleSshConfig(alias: string, host: string): string {
|
||||
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
|
||||
}
|
||||
|
||||
private generateSshConfig(configs: string[]) {
|
||||
try {
|
||||
for (const config of configs) {
|
||||
fs.appendFileSync(this.sshConfigFilePath, config, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('写入ssh配置文件失败', error);
|
||||
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
|
||||
if (host === 'github.com') {
|
||||
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa`;
|
||||
}
|
||||
const proxyStr = proxy
|
||||
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
|
||||
: '';
|
||||
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
|
||||
this.sshPath,
|
||||
alias,
|
||||
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
||||
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
|
||||
private removeSshConfig(alias: string) {
|
||||
try {
|
||||
const configRegx = this.getConfigRegx(alias);
|
||||
const data = fs
|
||||
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
|
||||
.replace(configRegx, '')
|
||||
.replace(/\n[\n]+/g, '\n');
|
||||
fs.writeFileSync(this.sshConfigFilePath, data, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const filePath = path.join(this.sshPath, `${alias}.config`);
|
||||
if (existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`删除ssh配置文件${alias}失败`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public addSSHKey(key: string, alias: string, host: string): void {
|
||||
public addSSHKey(
|
||||
key: string,
|
||||
alias: string,
|
||||
host: string,
|
||||
proxy?: string,
|
||||
): void {
|
||||
this.generatePrivateKeyFile(alias, key);
|
||||
const config = this.generateSingleSshConfig(alias, host);
|
||||
this.removeSshConfig(alias);
|
||||
this.generateSshConfig([config]);
|
||||
this.generateSingleSshConfig(alias, host, proxy);
|
||||
}
|
||||
|
||||
public removeSSHKey(alias: string, host: string): void {
|
||||
public removeSSHKey(alias: string, host: string, proxy?: string): void {
|
||||
this.removePrivateKeyFile(alias);
|
||||
const config = this.generateSingleSshConfig(alias, host);
|
||||
this.removeSshConfig(config);
|
||||
this.removeSshConfig(alias);
|
||||
}
|
||||
|
||||
public setSshConfig(docs: Subscription[]) {
|
||||
for (const doc of docs) {
|
||||
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
|
||||
const { alias, proxy } = doc;
|
||||
const { host } = formatUrl(doc);
|
||||
this.removePrivateKeyFile(alias);
|
||||
this.removeSshConfig(alias);
|
||||
this.generatePrivateKeyFile(
|
||||
alias,
|
||||
(doc.pull_option as any).private_key,
|
||||
);
|
||||
this.generateSingleSshConfig(alias, host, proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+65
-149
@@ -6,29 +6,30 @@ import {
|
||||
SubscriptionModel,
|
||||
SubscriptionStatus,
|
||||
} from '../data/subscription';
|
||||
import {
|
||||
ChildProcessWithoutNullStreams,
|
||||
exec,
|
||||
execSync,
|
||||
spawn,
|
||||
} from 'child_process';
|
||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import cron_parser from 'cron-parser';
|
||||
import {
|
||||
getFileContentByName,
|
||||
concurrentRun,
|
||||
fileExist,
|
||||
createFile,
|
||||
killTask,
|
||||
handleLogPath,
|
||||
promiseExec,
|
||||
emptyDir,
|
||||
} from '../config/util';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { Op } from 'sequelize';
|
||||
import path from 'path';
|
||||
import { FindOptions, Op } from 'sequelize';
|
||||
import path, { join } from 'path';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
||||
import SockService from './sock';
|
||||
import SshKeyService from './sshKey';
|
||||
import dayjs from 'dayjs';
|
||||
import { LOG_END_SYMBOL } from '../config/const';
|
||||
import { formatCommand, formatUrl } from '../config/subscription';
|
||||
import { CrontabModel } from '../data/cron';
|
||||
import CrontabService from './cron';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionService {
|
||||
@@ -37,7 +38,8 @@ export default class SubscriptionService {
|
||||
private scheduleService: ScheduleService,
|
||||
private sockService: SockService,
|
||||
private sshKeyService: SshKeyService,
|
||||
) {}
|
||||
private crontabService: CrontabService,
|
||||
) { }
|
||||
|
||||
public async list(searchText?: string): Promise<Subscription[]> {
|
||||
let query = {};
|
||||
@@ -45,7 +47,7 @@ export default class SubscriptionService {
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||
{ [Op.like]: `%${encodeURI(searchText)}%` },
|
||||
],
|
||||
};
|
||||
query = {
|
||||
@@ -62,64 +64,25 @@ export default class SubscriptionService {
|
||||
try {
|
||||
const result = await SubscriptionModel.findAll({
|
||||
where: query,
|
||||
order: [['createdAt', 'DESC']],
|
||||
order: [
|
||||
['is_disabled', 'ASC'],
|
||||
['createdAt', 'DESC'],
|
||||
],
|
||||
});
|
||||
return result as any;
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private formatCommand(doc: Subscription, url?: string) {
|
||||
let command = 'ql ';
|
||||
let _url = url || this.formatUrl(doc).url;
|
||||
const { type, whitelist, blacklist, dependences, branch, extensions } = doc;
|
||||
if (type === 'file') {
|
||||
command += `raw "${_url}"`;
|
||||
} else {
|
||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
||||
dependences || ''
|
||||
}" "${branch || ''}" "${extensions || ''}"`;
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
private formatUrl(doc: Subscription) {
|
||||
let url = doc.url;
|
||||
let host = '';
|
||||
if (doc.type === 'private-repo') {
|
||||
if (doc.pull_type === 'ssh-key') {
|
||||
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
|
||||
url = doc.url!.replace(host, doc.alias);
|
||||
} else {
|
||||
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
|
||||
const { username, password } = doc.pull_option as any;
|
||||
url = doc.url!.replace(host, `${username}:${password}@${host}`);
|
||||
}
|
||||
}
|
||||
return { url, host };
|
||||
}
|
||||
|
||||
public async handleTask(
|
||||
doc: Subscription,
|
||||
needCreate = true,
|
||||
needAddKey = true,
|
||||
runImmediately = false,
|
||||
) {
|
||||
const { url, host } = this.formatUrl(doc);
|
||||
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
|
||||
if (needAddKey) {
|
||||
this.sshKeyService.addSSHKey(
|
||||
(doc.pull_option as any).private_key,
|
||||
doc.alias,
|
||||
host,
|
||||
);
|
||||
} else {
|
||||
this.sshKeyService.removeSSHKey(doc.alias, host);
|
||||
}
|
||||
}
|
||||
const { url } = formatUrl(doc);
|
||||
|
||||
doc.command = this.formatCommand(doc, url as string);
|
||||
doc.command = formatCommand(doc, url as string);
|
||||
|
||||
if (doc.schedule_type === 'crontab') {
|
||||
this.scheduleService.cancelCronTask(doc as any);
|
||||
@@ -142,28 +105,9 @@ export default class SubscriptionService {
|
||||
}
|
||||
}
|
||||
|
||||
private async promiseExec(command: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
command,
|
||||
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
|
||||
(err, stdout, stderr) => {
|
||||
resolve(stdout || stderr || JSON.stringify(err));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleLogPath(
|
||||
logPath: string,
|
||||
data: string = '',
|
||||
): Promise<string> {
|
||||
const absolutePath = path.resolve(config.logPath, logPath);
|
||||
const logFileExist = await fileExist(absolutePath);
|
||||
if (!logFileExist) {
|
||||
await createFile(absolutePath, data);
|
||||
}
|
||||
return absolutePath;
|
||||
public async setSshConfig() {
|
||||
const docs = await SubscriptionModel.findAll();
|
||||
this.sshKeyService.setSshConfig(docs);
|
||||
}
|
||||
|
||||
private taskCallbacks(doc: Subscription): TaskCallbacks {
|
||||
@@ -178,7 +122,7 @@ export default class SubscriptionService {
|
||||
},
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
const absolutePath = await this.handleLogPath(
|
||||
const absolutePath = await handleLogPath(
|
||||
logPath as string,
|
||||
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
|
||||
);
|
||||
@@ -188,7 +132,7 @@ export default class SubscriptionService {
|
||||
try {
|
||||
if (doc.sub_before) {
|
||||
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
|
||||
beforeStr = await this.promiseExec(doc.sub_before);
|
||||
beforeStr = await promiseExec(doc.sub_before);
|
||||
}
|
||||
} catch (error: any) {
|
||||
beforeStr =
|
||||
@@ -208,14 +152,14 @@ export default class SubscriptionService {
|
||||
},
|
||||
onEnd: async (cp, endTime, diff) => {
|
||||
const sub = await this.getDb({ id: doc.id });
|
||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||
|
||||
// 执行 sub_after
|
||||
let afterStr = '';
|
||||
try {
|
||||
if (sub.sub_after) {
|
||||
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
|
||||
afterStr = await this.promiseExec(sub.sub_after);
|
||||
afterStr = await promiseExec(sub.sub_after);
|
||||
}
|
||||
} catch (error: any) {
|
||||
afterStr =
|
||||
@@ -229,7 +173,7 @@ export default class SubscriptionService {
|
||||
absolutePath,
|
||||
`\n## 执行结束... ${endTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)} 耗时 ${diff} 秒`,
|
||||
)} 耗时 ${diff} 秒${LOG_END_SYMBOL}`,
|
||||
);
|
||||
|
||||
await SubscriptionModel.update(
|
||||
@@ -245,12 +189,12 @@ export default class SubscriptionService {
|
||||
},
|
||||
onError: async (message: string) => {
|
||||
const sub = await this.getDb({ id: doc.id });
|
||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
},
|
||||
onLog: async (message: string) => {
|
||||
const sub = await this.getDb({ id: doc.id });
|
||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
},
|
||||
};
|
||||
@@ -260,6 +204,7 @@ export default class SubscriptionService {
|
||||
const tab = new Subscription(payload);
|
||||
const doc = await this.insert(tab);
|
||||
await this.handleTask(doc);
|
||||
await this.setSshConfig();
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -268,8 +213,11 @@ export default class SubscriptionService {
|
||||
}
|
||||
|
||||
public async update(payload: Subscription): Promise<Subscription> {
|
||||
const newDoc = await this.updateDb(payload);
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new Subscription({ ...doc, ...payload });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
await this.handleTask(newDoc, !newDoc.is_disabled);
|
||||
await this.setSshConfig();
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
@@ -309,15 +257,29 @@ export default class SubscriptionService {
|
||||
);
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
public async remove(ids: number[], query: any) {
|
||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
await this.handleTask(doc, false, false);
|
||||
await this.handleTask(doc, false);
|
||||
}
|
||||
await SubscriptionModel.destroy({ where: { id: ids } });
|
||||
await this.setSshConfig();
|
||||
|
||||
if (query?.force === true) {
|
||||
const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
|
||||
if (crons?.length) {
|
||||
await this.crontabService.remove(crons.map(x => x.id!))
|
||||
}
|
||||
for (const doc of docs) {
|
||||
const filePath = join(config.scriptPath, doc.alias);
|
||||
emptyDir(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<Subscription> {
|
||||
public async getDb(
|
||||
query: FindOptions<Subscription>['where'],
|
||||
): Promise<Subscription> {
|
||||
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Subscription);
|
||||
}
|
||||
@@ -327,10 +289,9 @@ export default class SubscriptionService {
|
||||
{ status: SubscriptionStatus.queued },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
concurrentRun(
|
||||
ids.map((id) => async () => await this.runSingle(id)),
|
||||
10,
|
||||
);
|
||||
ids.forEach((id) => {
|
||||
this.runSingle(id);
|
||||
});
|
||||
}
|
||||
|
||||
public async stop(ids: number[]) {
|
||||
@@ -338,20 +299,11 @@ export default class SubscriptionService {
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
try {
|
||||
process.kill(-doc.pid);
|
||||
await killTask(doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.silly(error);
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
const command = this.formatCommand(doc);
|
||||
const err = await this.killTask(command);
|
||||
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
||||
const str = err ? `\n${err}` : '';
|
||||
|
||||
fs.appendFileSync(
|
||||
`${absolutePath}`,
|
||||
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `,
|
||||
);
|
||||
}
|
||||
|
||||
await SubscriptionModel.update(
|
||||
@@ -360,69 +312,33 @@ export default class SubscriptionService {
|
||||
);
|
||||
}
|
||||
|
||||
public async killTask(name: string) {
|
||||
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
||||
const execAsync = promisify(exec);
|
||||
try {
|
||||
let pid = (await execAsync(taskCommand)).stdout;
|
||||
if (pid) {
|
||||
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let pids = pid.match(/\(\d+/g);
|
||||
const killLogs = [];
|
||||
if (pids && pids.length > 0) {
|
||||
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
|
||||
for (const id of pids) {
|
||||
const c = `kill -9 ${id.slice(1)}`;
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(c);
|
||||
if (stderr) {
|
||||
killLogs.push(stderr);
|
||||
}
|
||||
if (stdout) {
|
||||
killLogs.push(stdout);
|
||||
}
|
||||
} catch (error: any) {
|
||||
killLogs.push(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
||||
} catch (e) {
|
||||
return JSON.stringify(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async runSingle(subscriptionId: number) {
|
||||
const subscription = await this.getDb({ id: subscriptionId });
|
||||
if (subscription.status !== SubscriptionStatus.queued) {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = this.formatCommand(subscription);
|
||||
const command = formatCommand(subscription);
|
||||
|
||||
await this.scheduleService.runTask(
|
||||
command,
|
||||
this.taskCallbacks(subscription),
|
||||
);
|
||||
this.scheduleService.runTask(command, this.taskCallbacks(subscription));
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||
await this.setSshConfig();
|
||||
for (const doc of docs) {
|
||||
await this.handleTask(doc, false);
|
||||
}
|
||||
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||
await this.setSshConfig();
|
||||
for (const doc of docs) {
|
||||
await this.handleTask(doc);
|
||||
}
|
||||
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async log(id: number) {
|
||||
@@ -431,7 +347,7 @@ export default class SubscriptionService {
|
||||
return '';
|
||||
}
|
||||
|
||||
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
||||
const absolutePath = await handleLogPath(doc.log_path as string);
|
||||
return getFileContentByName(absolutePath);
|
||||
}
|
||||
|
||||
|
||||
+180
-51
@@ -1,15 +1,34 @@
|
||||
import { Response } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||
import {
|
||||
AuthDataType,
|
||||
AuthInfo,
|
||||
AuthInstance,
|
||||
AuthModel,
|
||||
AuthModelInfo,
|
||||
} from '../data/auth';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import NotificationService from './notify';
|
||||
import ScheduleService from './schedule';
|
||||
import { spawn } from 'child_process';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import { spawn } from 'cross-spawn';
|
||||
import SockService from './sock';
|
||||
import got from 'got';
|
||||
import {
|
||||
getPid,
|
||||
killTask,
|
||||
parseContentVersion,
|
||||
parseVersion,
|
||||
promiseExec,
|
||||
readDirs,
|
||||
} from '../config/util';
|
||||
import { TASK_COMMAND } from '../config/const';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import tar from 'tar';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { sum } from 'lodash';
|
||||
|
||||
@Service()
|
||||
export default class SystemService {
|
||||
@@ -22,20 +41,20 @@ export default class SystemService {
|
||||
private sockService: SockService,
|
||||
) {}
|
||||
|
||||
public async getLogRemoveFrequency() {
|
||||
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
|
||||
return doc || {};
|
||||
public async getSystemConfig() {
|
||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||
return doc || ({} as AuthInstance);
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<AuthInstance> {
|
||||
await AuthModel.upsert({ ...payload });
|
||||
const doc = await this.getDb({ type: payload.type });
|
||||
return doc;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<any> {
|
||||
public async getDb(query: any): Promise<AuthInstance> {
|
||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as any);
|
||||
return doc && doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||
@@ -56,56 +75,61 @@ export default class SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateLogRemoveFrequency(frequency: number) {
|
||||
const oDoc = await this.getLogRemoveFrequency();
|
||||
public async updateSystemConfig(info: AuthModelInfo) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
const result = await this.updateAuthDb({
|
||||
...oDoc,
|
||||
type: AuthDataType.removeLogFrequency,
|
||||
info: { frequency },
|
||||
type: AuthDataType.systemConfig,
|
||||
info,
|
||||
});
|
||||
const cron = {
|
||||
id: result.id,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${frequency}`,
|
||||
};
|
||||
await this.scheduleService.cancelIntervalTask(cron);
|
||||
if (frequency > 0) {
|
||||
await this.scheduleService.createIntervalTask(cron, {
|
||||
days: frequency,
|
||||
runImmediately: true,
|
||||
});
|
||||
if (info.logRemoveFrequency) {
|
||||
const cron = {
|
||||
id: result.id,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${info.logRemoveFrequency}`,
|
||||
};
|
||||
await this.scheduleService.cancelIntervalTask(cron);
|
||||
if (info.logRemoveFrequency > 0) {
|
||||
this.scheduleService.createIntervalTask(cron, {
|
||||
days: info.logRemoveFrequency,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { code: 200, data: { ...cron } };
|
||||
if (info.cronConcurrency) {
|
||||
await taskLimit.setCustomLimit(info.cronConcurrency);
|
||||
}
|
||||
return { code: 200, data: info };
|
||||
}
|
||||
|
||||
public async checkUpdate() {
|
||||
try {
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
||||
const currentVersionContent = await parseVersion(config.versionFile);
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
let lastVersion = '';
|
||||
let lastLog = '';
|
||||
let lastVersionContent;
|
||||
try {
|
||||
const result = await got.get(config.lastVersionFile, {
|
||||
timeout: 6000,
|
||||
retry: 0,
|
||||
});
|
||||
const lastVersionFileContent = result.body;
|
||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||
lastLog = lastVersionFileContent.match(logRegx)
|
||||
? lastVersionFileContent.match(logRegx)![1]
|
||||
: '';
|
||||
const result = await got.get(
|
||||
`${config.lastVersionFile}?t=${Date.now()}`,
|
||||
{
|
||||
timeout: 30000,
|
||||
},
|
||||
);
|
||||
lastVersionContent = await parseContentVersion(result.body);
|
||||
} catch (error) {}
|
||||
|
||||
if (!lastVersionContent) {
|
||||
lastVersionContent = currentVersionContent;
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
||||
lastVersion,
|
||||
lastLog,
|
||||
hasNewVersion: this.checkHasNewVersion(
|
||||
currentVersionContent.version,
|
||||
lastVersionContent.version,
|
||||
),
|
||||
lastVersion: lastVersionContent.version,
|
||||
lastLog: lastVersionContent.changeLog,
|
||||
lastLogLink: lastVersionContent.changeLogLink,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
@@ -136,12 +160,8 @@ export default class SystemService {
|
||||
}
|
||||
|
||||
public async updateSystem() {
|
||||
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
||||
const cp = spawn('ql -l update false', { shell: '/bin/bash' });
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: `开始更新系统`,
|
||||
});
|
||||
cp.stdout.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
@@ -166,6 +186,33 @@ export default class SystemService {
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async reloadSystem(target: 'system' | 'data') {
|
||||
const cp = spawn(`ql -l reload ${target || ''}`, { shell: '/bin/bash' });
|
||||
|
||||
cp.stdout.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'reloadSystem',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'reloadSystem',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.on('error', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'reloadSystem',
|
||||
message: JSON.stringify(err),
|
||||
});
|
||||
});
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async notify({ title, content }: { title: string; content: string }) {
|
||||
const isSuccess = await this.notificationService.notify(title, content);
|
||||
if (isSuccess) {
|
||||
@@ -174,4 +221,86 @@ export default class SystemService {
|
||||
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
|
||||
}
|
||||
}
|
||||
|
||||
public async run(
|
||||
{ command, logPath }: { command: string; logPath: string },
|
||||
callback: TaskCallbacks,
|
||||
) {
|
||||
if (!command.startsWith(TASK_COMMAND)) {
|
||||
command = `${TASK_COMMAND} ${command}`;
|
||||
}
|
||||
this.scheduleService.runTask(
|
||||
`real_log_path=${logPath} real_time=true ${command}`,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
public async stop({ command, pid }: { command: string; pid: number }) {
|
||||
if (!pid && !command) {
|
||||
return { code: 400, message: '参数错误' };
|
||||
}
|
||||
|
||||
if (pid) {
|
||||
await killTask(pid);
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
if (!command.startsWith(TASK_COMMAND)) {
|
||||
command = `${TASK_COMMAND} ${command}`;
|
||||
}
|
||||
const _pid = await getPid(command);
|
||||
if (_pid) {
|
||||
await killTask(_pid);
|
||||
return { code: 200 };
|
||||
} else {
|
||||
return { code: 400, message: '任务未找到' };
|
||||
}
|
||||
}
|
||||
|
||||
public async exportData(res: Response) {
|
||||
try {
|
||||
await tar.create(
|
||||
{ gzip: true, file: config.dataTgzFile, cwd: config.rootPath },
|
||||
['data'],
|
||||
);
|
||||
res.download(config.dataTgzFile);
|
||||
} catch (error: any) {
|
||||
return res.send({ code: 400, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
public async importData() {
|
||||
try {
|
||||
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
|
||||
await tar.x({ file: config.dataTgzFile, cwd: config.tmpPath });
|
||||
return { code: 200 };
|
||||
} catch (error: any) {
|
||||
return { code: 400, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
public async getSystemLog(res: Response) {
|
||||
const result = readDirs(config.systemLogPath, config.systemLogPath);
|
||||
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
|
||||
res.set({
|
||||
'Content-Length': sum(logs.map((x) => x.size)),
|
||||
});
|
||||
(function sendFiles(res, fileNames) {
|
||||
if (fileNames.length === 0) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLog = fileNames.shift();
|
||||
if (currentLog) {
|
||||
const currentFileStream = fs.createReadStream(
|
||||
path.join(config.systemLogPath, currentLog.title),
|
||||
);
|
||||
currentFileStream.on('end', () => {
|
||||
sendFiles(res, fileNames);
|
||||
});
|
||||
currentFileStream.pipe(res, { end: false });
|
||||
}
|
||||
})(res, logs);
|
||||
}
|
||||
}
|
||||
|
||||
+66
-36
@@ -1,17 +1,27 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
||||
import {
|
||||
createRandomString,
|
||||
fileExist,
|
||||
getNetIp,
|
||||
getPlatform,
|
||||
safeJSONParse,
|
||||
} from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { authenticator } from '@otplib/preset-default';
|
||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||
import {
|
||||
AuthDataType,
|
||||
AuthInfo,
|
||||
AuthModel,
|
||||
AuthModelInfo,
|
||||
LoginStatus,
|
||||
} from '../data/auth';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import NotificationService from './notify';
|
||||
import { Request } from 'express';
|
||||
import ScheduleService from './schedule';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -65,32 +75,37 @@ export default class UserService {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
|
||||
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
|
||||
const retriesTime = Math.pow(3, retries) * 1000;
|
||||
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
||||
const waitTime = Math.ceil(
|
||||
(retriesTime - (timestamp - lastlogon)) / 1000,
|
||||
);
|
||||
return {
|
||||
code: 410,
|
||||
message: `失败次数过多,请${Math.round(
|
||||
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||
)}秒后重试`,
|
||||
data: Math.round(
|
||||
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||
),
|
||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||
data: waitTime,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
username === cUsername &&
|
||||
password === cPassword &&
|
||||
twoFactorActivated &&
|
||||
needTwoFactor
|
||||
) {
|
||||
this.updateAuthInfo(content, {
|
||||
isTwoFactorChecking: true,
|
||||
});
|
||||
return {
|
||||
code: 420,
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
|
||||
const { ip, address } = await getNetIp(req);
|
||||
if (username === cUsername && password === cPassword) {
|
||||
if (twoFactorActivated && needTwoFactor) {
|
||||
this.updateAuthInfo(content, {
|
||||
isTwoFactorChecking: true,
|
||||
});
|
||||
return {
|
||||
code: 420,
|
||||
message: '请输入两步验证token',
|
||||
};
|
||||
}
|
||||
|
||||
const data = createRandomString(50, 100);
|
||||
const expiration = twoFactorActivated ? 30 : 3;
|
||||
const expiration = twoFactorActivated ? 60 : 20;
|
||||
let token = jwt.sign({ data }, config.secret as any, {
|
||||
expiresIn: 60 * 60 * 24 * expiration,
|
||||
algorithm: 'HS384',
|
||||
@@ -155,7 +170,16 @@ export default class UserService {
|
||||
status: LoginStatus.fail,
|
||||
},
|
||||
});
|
||||
return { code: 400, message: config.authError };
|
||||
if (retries > 2) {
|
||||
const waitTime = Math.round(Math.pow(3, retries + 1));
|
||||
return {
|
||||
code: 410,
|
||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||
data: waitTime,
|
||||
};
|
||||
} else {
|
||||
return { code: 400, message: config.authError };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return this.initAuthInfo();
|
||||
@@ -170,12 +194,14 @@ export default class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
public async getLoginLog(): Promise<AuthInfo[]> {
|
||||
public async getLoginLog(): Promise<Array<AuthModelInfo | undefined>> {
|
||||
const docs = await AuthModel.findAll({
|
||||
where: { type: AuthDataType.loginLog },
|
||||
});
|
||||
if (docs && docs.length > 0) {
|
||||
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
|
||||
const result = docs.sort(
|
||||
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
|
||||
);
|
||||
if (result.length > 100) {
|
||||
await AuthModel.destroy({
|
||||
where: { id: result[result.length - 1].id },
|
||||
@@ -192,17 +218,16 @@ export default class UserService {
|
||||
}
|
||||
|
||||
private initAuthInfo() {
|
||||
const newPassword = createRandomString(16, 22);
|
||||
fs.writeFileSync(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
password: newPassword,
|
||||
password: 'admin',
|
||||
}),
|
||||
);
|
||||
return {
|
||||
code: 100,
|
||||
message: '已初始化密码,请前往auth.json查看并重新登录',
|
||||
message: '未找到认证文件,重新初始化',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,13 +252,18 @@ export default class UserService {
|
||||
return { code: 200, data: avatar, message: '更新成功' };
|
||||
}
|
||||
|
||||
public getUserInfo(): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
||||
if (err) console.log(err);
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
public async getUserInfo(): Promise<any> {
|
||||
const authFileExist = await fileExist(config.authConfigFile);
|
||||
if (!authFileExist) {
|
||||
fs.writeFileSync(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return this.getAuthInfo();
|
||||
}
|
||||
|
||||
public initTwoFactor() {
|
||||
@@ -298,7 +328,7 @@ export default class UserService {
|
||||
|
||||
private getAuthInfo() {
|
||||
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
return JSON.parse(content || '{}');
|
||||
return safeJSONParse(content);
|
||||
}
|
||||
|
||||
private updateAuthInfo(authInfo: any, info: any) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import pLimit from 'p-limit';
|
||||
import os from 'os';
|
||||
import { AuthDataType, AuthModel } from '../data/auth';
|
||||
import Logger from '../loaders/logger';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
class TaskLimit {
|
||||
private oneLimit = pLimit(1);
|
||||
private updateLogLimit = pLimit(1);
|
||||
private cpuLimit = pLimit(Math.max(os.cpus().length, 4));
|
||||
|
||||
get cpuLimitActiveCount() {
|
||||
return this.cpuLimit.activeCount;
|
||||
}
|
||||
|
||||
get cpuLimitPendingCount() {
|
||||
return this.cpuLimit.pendingCount;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.setCustomLimit();
|
||||
}
|
||||
|
||||
public async setCustomLimit(limit?: number) {
|
||||
if (limit) {
|
||||
this.cpuLimit = pLimit(limit);
|
||||
return;
|
||||
}
|
||||
await AuthModel.sync();
|
||||
const doc = await AuthModel.findOne({
|
||||
where: { type: AuthDataType.systemConfig },
|
||||
});
|
||||
if (doc?.info?.cronConcurrency) {
|
||||
this.cpuLimit = pLimit(doc?.info?.cronConcurrency);
|
||||
}
|
||||
}
|
||||
|
||||
public runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> {
|
||||
Logger.info(
|
||||
`[schedule][任务加入队列] 运行中任务数: ${this.cpuLimitActiveCount}, 等待中任务数: ${this.cpuLimitPendingCount}`,
|
||||
);
|
||||
return this.cpuLimit(fn);
|
||||
}
|
||||
|
||||
public runOneByOne<T>(fn: () => Promise<T>): Promise<T> {
|
||||
return this.oneLimit(fn);
|
||||
}
|
||||
|
||||
public updateDepLog<T>(fn: () => Promise<T>): Promise<T> {
|
||||
return this.updateLogLimit(fn);
|
||||
}
|
||||
}
|
||||
|
||||
export default new TaskLimit();
|
||||
@@ -0,0 +1,33 @@
|
||||
import { spawn } from 'cross-spawn';
|
||||
import taskLimit from './pLimit';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
export function runCron(cmd: string): Promise<number> {
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
return new Promise(async (resolve: any) => {
|
||||
Logger.info(`[schedule][开始执行任务] 运行命令: ${cmd}`);
|
||||
|
||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
Logger.info(
|
||||
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
||||
cmd,
|
||||
data.toString(),
|
||||
);
|
||||
});
|
||||
cp.on('error', (err) => {
|
||||
Logger.error(
|
||||
'[schedule][创建任务失败] 命令: %s, 错误信息: %j',
|
||||
cmd,
|
||||
err,
|
||||
);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
Logger.info(`[schedule][任务退出] ${cmd} 进程id: ${cp.pid} 退出, 退出码 ${code}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+9
-3
@@ -5,6 +5,7 @@ import LoggerInstance from './loaders/logger';
|
||||
import fs from 'fs';
|
||||
import config from './config';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
const tokenFile = path.join(config.configPath, 'token.json');
|
||||
|
||||
@@ -25,9 +26,14 @@ async function getToken() {
|
||||
|
||||
async function writeFile(data: any) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => {
|
||||
resolve();
|
||||
});
|
||||
fs.writeFile(
|
||||
tokenFile,
|
||||
`${JSON.stringify(data)}${os.EOL}`,
|
||||
{ encoding: 'utf8' },
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+68
-50
@@ -1,60 +1,78 @@
|
||||
FROM node:alpine
|
||||
FROM python:3.10-alpine as builder
|
||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||
RUN set -x \
|
||||
&& apk update \
|
||||
&& apk add nodejs npm git \
|
||||
&& npm i -g pnpm@8.3.1 \
|
||||
&& cd /tmp/build \
|
||||
&& pnpm --registry https://registry.npmmirror.com install --prod
|
||||
|
||||
FROM python:3.10-alpine
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||
ARG QL_BRANCH=master
|
||||
ARG QL_BRANCH=develop
|
||||
|
||||
ENV PNPM_HOME=/root/.local/share/pnpm \
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
|
||||
LANG=zh_CN.UTF-8 \
|
||||
SHELL=/bin/bash \
|
||||
PS1="\u@\h:\w \$ " \
|
||||
QL_DIR=/ql \
|
||||
QL_BRANCH=${QL_BRANCH}
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
|
||||
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
|
||||
LANG=C.UTF-8 \
|
||||
SHELL=/bin/bash \
|
||||
PS1="\u@\h:\w \$ " \
|
||||
QL_DIR=/ql \
|
||||
QL_BRANCH=${QL_BRANCH}
|
||||
|
||||
RUN set -x \
|
||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update -f \
|
||||
&& apk upgrade \
|
||||
&& apk --no-cache add -f bash \
|
||||
coreutils \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
tzdata \
|
||||
perl \
|
||||
openssl \
|
||||
nginx \
|
||||
nodejs \
|
||||
jq \
|
||||
openssh \
|
||||
procps \
|
||||
netcat-openbsd \
|
||||
npm \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& apk update \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||
&& git config --global user.name "qinglong" \
|
||||
&& git config --global http.postBuffer 524288000 \
|
||||
&& npm install -g pnpm@8.3.1 \
|
||||
&& cd && pnpm config set registry https://registry.npmmirror.com \
|
||||
&& pnpm add -g pm2 tsx \
|
||||
&& rm -rf /root/.pnpm-store \
|
||||
&& rm -rf /root/.local/share/pnpm/store \
|
||||
&& rm -rf /root/.cache \
|
||||
&& rm -rf /root/.npm
|
||||
|
||||
ARG SOURCE_COMMIT
|
||||
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
&& cd ${QL_DIR} \
|
||||
&& cp -f .env.example .env \
|
||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||
&& mkdir -p ${QL_DIR}/static \
|
||||
&& cp -rf /static/* ${QL_DIR}/static \
|
||||
&& rm -rf /static
|
||||
|
||||
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||
|
||||
WORKDIR ${QL_DIR}
|
||||
|
||||
RUN set -x \
|
||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update -f \
|
||||
&& apk upgrade \
|
||||
&& apk --no-cache add -f bash \
|
||||
coreutils \
|
||||
moreutils \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
tzdata \
|
||||
perl \
|
||||
openssl \
|
||||
nginx \
|
||||
python3 \
|
||||
jq \
|
||||
openssh \
|
||||
py3-pip \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& apk update \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||
&& git config --global user.name "qinglong" \
|
||||
&& git config --global http.postBuffer 524288000 \
|
||||
&& npm install -g pnpm \
|
||||
&& pnpm add -g pm2 ts-node typescript tslib \
|
||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
&& cd ${QL_DIR} \
|
||||
&& cp -f .env.example .env \
|
||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||
&& pnpm install --prod \
|
||||
&& rm -rf /root/.pnpm-store \
|
||||
&& rm -rf /root/.local/share/pnpm/store \
|
||||
&& rm -rf /root/.cache \
|
||||
&& rm -rf /root/.npm \
|
||||
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||
&& mkdir -p ${QL_DIR}/static \
|
||||
&& cp -rf /static/* ${QL_DIR}/static \
|
||||
&& rm -rf /static
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||
CMD curl -sf http://127.0.0.1:5400/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
version: '2'
|
||||
services:
|
||||
web:
|
||||
# alpine 基础镜像版本
|
||||
image: whyour/qinglong:latest
|
||||
# debian-slim 基础镜像版本
|
||||
# image: whyour/qinglong:debian
|
||||
volumes:
|
||||
- ./data:/ql/data
|
||||
ports:
|
||||
- "0.0.0.0:5700:5700"
|
||||
environment:
|
||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
QlBaseUrl: '/'
|
||||
restart: unless-stopped
|
||||
|
||||
+23
-23
@@ -7,49 +7,49 @@ link_shell
|
||||
echo -e "======================1. 检测配置文件========================\n"
|
||||
make_dir /etc/nginx/conf.d
|
||||
make_dir /run/nginx
|
||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||
init_nginx
|
||||
fix_config
|
||||
|
||||
pm2 l &>/dev/null
|
||||
echo
|
||||
|
||||
echo -e "======================2. 安装依赖========================\n"
|
||||
update_depend
|
||||
patch_version
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
current_npm_registry=$(cd && pnpm config get registry)
|
||||
is_equal_registry=$(echo $current_npm_registry | grep "${NpmMirror}")
|
||||
if [[ "$is_equal_registry" == "" ]]; then
|
||||
cd && pnpm config set registry $NpmMirror
|
||||
pnpm install -g
|
||||
fi
|
||||
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
|
||||
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 public &>/dev/null
|
||||
pm2 start $dir_static/build/public.js -n public --source-map-support --time
|
||||
echo -e "监控服务启动成功...\n"
|
||||
|
||||
echo -e "======================5. 启动控制面板========================\n"
|
||||
pm2 delete panel &>/dev/null
|
||||
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
|
||||
echo -e "控制面板启动成功...\n"
|
||||
|
||||
echo -e "======================6. 启动定时任务========================\n"
|
||||
pm2 delete schedule &>/dev/null
|
||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
|
||||
echo -e "定时任务启动成功...\n"
|
||||
echo -e "======================4. 启动pm2服务========================\n"
|
||||
reload_pm2
|
||||
|
||||
if [[ $AutoStartBot == true ]]; then
|
||||
echo -e "======================7. 启动bot========================\n"
|
||||
nohup ql bot >>$dir_log/start.log 2>&1 &
|
||||
echo -e "======================5. 启动bot========================\n"
|
||||
nohup ql -l bot >$dir_log/bot.log 2>&1 &
|
||||
echo -e "bot后台启动中...\n"
|
||||
fi
|
||||
|
||||
if [[ $EnableExtraShell == true ]]; then
|
||||
echo -e "======================8. 执行自定义脚本========================\n"
|
||||
nohup ql extra >>$dir_log/start.log 2>&1 &
|
||||
echo -e "====================6. 执行自定义脚本========================\n"
|
||||
nohup ql -l extra >$dir_log/extra.log 2>&1 &
|
||||
echo -e "自定义脚本后台执行中...\n"
|
||||
fi
|
||||
|
||||
echo -e "############################################################\n"
|
||||
echo -e "容器启动成功..."
|
||||
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
|
||||
echo -e "############################################################\n"
|
||||
|
||||
crond -f >/dev/null
|
||||
|
||||
+21
-16
@@ -1,57 +1,62 @@
|
||||
upstream api {
|
||||
upstream baseApi {
|
||||
server 0.0.0.0:5600;
|
||||
}
|
||||
|
||||
upstream public {
|
||||
upstream publicApi {
|
||||
server 0.0.0.0:5400;
|
||||
}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default keep-alive;
|
||||
'websocket' upgrade;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 5700;
|
||||
root /ql/static/dist;
|
||||
IPV6_CONFIG
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
location /api/public {
|
||||
location QL_BASE_URLapi/public/ {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://public;
|
||||
proxy_pass http://publicApi/api/;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /api {
|
||||
location QL_BASE_URLapi/ {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://api;
|
||||
proxy_pass http://baseApi/api/;
|
||||
proxy_buffering off;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
location /open {
|
||||
location QL_BASE_URLopen/ {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://api;
|
||||
proxy_pass http://baseApi/open/;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_static on;
|
||||
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
||||
gzip_proxied any;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.0;
|
||||
gzip_http_version 1.0;
|
||||
QL_ROOT_CONFIG
|
||||
|
||||
location / {
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
location QL_BASE_URL_LOCATION {
|
||||
QL_ALIAS_CONFIG
|
||||
index index.html index.htm;
|
||||
try_files $uri QL_BASE_URLindex.html;
|
||||
}
|
||||
|
||||
location ~ .*\.(html)$ {
|
||||
|
||||
+19
-19
@@ -5,41 +5,41 @@ error_log /var/log/nginx/error.log warn;
|
||||
include /etc/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
server_tokens off;
|
||||
server_tokens off;
|
||||
|
||||
client_max_body_size 20m;
|
||||
client_body_buffer_size 20m;
|
||||
client_max_body_size 4096m;
|
||||
client_body_buffer_size 20m;
|
||||
|
||||
keepalive_timeout 65;
|
||||
keepalive_timeout 65;
|
||||
|
||||
sendfile on;
|
||||
sendfile on;
|
||||
|
||||
tcp_nodelay on;
|
||||
tcp_nodelay on;
|
||||
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
ssl_session_cache shared:SSL:2m;
|
||||
ssl_session_cache shared:SSL:2m;
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
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_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"';
|
||||
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;
|
||||
access_log /var/log/nginx/access.log main;
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'schedule',
|
||||
max_restarts: 10,
|
||||
kill_timeout: 15000,
|
||||
wait_ready: true,
|
||||
listen_timeout: 10000,
|
||||
source_map_support: true,
|
||||
time: true,
|
||||
script: 'static/build/schedule/index.js',
|
||||
},
|
||||
{
|
||||
name: 'public',
|
||||
max_restarts: 10,
|
||||
kill_timeout: 15000,
|
||||
wait_ready: true,
|
||||
listen_timeout: 10000,
|
||||
source_map_support: true,
|
||||
time: true,
|
||||
script: 'static/build/public.js',
|
||||
},
|
||||
{
|
||||
name: 'panel',
|
||||
max_restarts: 10,
|
||||
kill_timeout: 15000,
|
||||
wait_ready: true,
|
||||
listen_timeout: 10000,
|
||||
source_map_support: true,
|
||||
time: true,
|
||||
script: 'static/build/app.js',
|
||||
},
|
||||
],
|
||||
};
|
||||
+74
-30
@@ -2,16 +2,18 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "concurrently -n w: npm:start:*",
|
||||
"start:front": "umi dev",
|
||||
"start:front": "max dev",
|
||||
"start:back": "nodemon",
|
||||
"start:public": "ts-node back/public.ts",
|
||||
"build:front": "umi build",
|
||||
"start:public": "ts-node --transpile-only ./back/public.ts",
|
||||
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts",
|
||||
"build:front": "max build",
|
||||
"build:back": "tsc -p tsconfig.back.json",
|
||||
"panel": "npm run build:back && node static/build/app.js",
|
||||
"schedule": "npm run build:back && node static/build/schedule.js",
|
||||
"schedule": "npm run build:back && node static/build/schedule/index.js",
|
||||
"public": "npm run build:back && node static/build/public.js",
|
||||
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"postinstall": "umi generate tmp 2>/dev/null || true",
|
||||
"postinstall": "max setup 2>/dev/null || true",
|
||||
"test": "umi-test",
|
||||
"test:coverage": "umi-test --coverage"
|
||||
},
|
||||
@@ -28,99 +30,141 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"react",
|
||||
"react-dom",
|
||||
"antd",
|
||||
"dva",
|
||||
"postcss",
|
||||
"webpack",
|
||||
"eslint",
|
||||
"stylelint",
|
||||
"redux",
|
||||
"@babel/core",
|
||||
"monaco-editor",
|
||||
"rc-field-form",
|
||||
"@types/lodash.merge",
|
||||
"rollup",
|
||||
"styled-components"
|
||||
],
|
||||
"allowedVersions": {
|
||||
"react": "17",
|
||||
"react-dom": "17"
|
||||
"react": "18",
|
||||
"react-dom": "18",
|
||||
"dva-core": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.8.13",
|
||||
"@otplib/preset-default": "^12.0.1",
|
||||
"@sentry/node": "^7.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
"@sentry/node": "^7.12.1",
|
||||
"@sentry/tracing": "^7.12.1",
|
||||
"body-parser": "^1.19.2",
|
||||
"celebrate": "^15.0.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^4.2.1",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"dayjs": "^1.11.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"express-jwt": "^6.1.1",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"express-urlrewrite": "^1.4.0",
|
||||
"form-data": "^4.0.0",
|
||||
"got": "^11.8.2",
|
||||
"hpagent": "^0.1.2",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^1.4.4",
|
||||
"nedb": "^1.8.0",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"p-queue": "7.2.0",
|
||||
"p-limit": "3.1.0",
|
||||
"protobufjs": "^7.2.3",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^6.20.1",
|
||||
"sequelize": "^6.25.5",
|
||||
"serve-handler": "^6.1.3",
|
||||
"sockjs": "^0.3.24",
|
||||
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
|
||||
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
|
||||
"tar": "^6.1.15",
|
||||
"toad-scheduler": "^1.6.0",
|
||||
"typedi": "^0.10.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.6.0",
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"yargs": "^17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/pro-layout": "^6.33.1",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@sentry/react": "^7.0.0",
|
||||
"@ant-design/pro-layout": "6.38.22",
|
||||
"@monaco-editor/react": "4.2.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@sentry/react": "^7.12.1",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-jwt": "^6.0.4",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/lodash": "^4.14.185",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/nedb": "^1.8.12",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/qrcode.react": "^1.0.2",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.13",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/serve-handler": "^6.1.1",
|
||||
"@types/sockjs": "^0.3.33",
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
"@types/tar": "^6.1.5",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@umijs/plugin-antd": "^0.15.0",
|
||||
"@umijs/plugin-esbuild": "^1.4.1",
|
||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||
"@uiw/react-codemirror": "^4.21.9",
|
||||
"@umijs/max": "^4.0.72",
|
||||
"@umijs/ssr-darkreader": "^4.9.45",
|
||||
"@umijs/test": "^3.5.21",
|
||||
"ahooks": "^3.7.8",
|
||||
"ansi-to-react": "^6.1.6",
|
||||
"antd": "^4.20.5",
|
||||
"antd": "^4.24.8",
|
||||
"antd-img-crop": "^4.2.3",
|
||||
"codemirror": "^5.65.2",
|
||||
"axios": "^1.4.0",
|
||||
"compression-webpack-plugin": "9.2.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"lint-staged": "^12.3.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"lint-staged": "^13.0.3",
|
||||
"monaco-editor": "0.33.0",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettier": "^2.5.1",
|
||||
"qiniu": "^7.4.0",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"query-string": "^7.1.1",
|
||||
"rc-tween-one": "^3.0.6",
|
||||
"react": "17.x",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"rc-virtual-list": "3.5.3",
|
||||
"react": "18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-diff-viewer": "^3.1.1",
|
||||
"react-dnd": "^14.0.2",
|
||||
"react-dnd-html5-backend": "^14.0.0",
|
||||
"react-dom": "17.x",
|
||||
"react-dom": "18.2.0",
|
||||
"react-intl-universal": "^2.6.21",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.6.0",
|
||||
"ts-node": "^10.6.0",
|
||||
"typescript": "^4.6.2",
|
||||
"umi": "^3.5.21",
|
||||
"umi-request": "^1.4.0",
|
||||
"ts-proto": "^1.146.0",
|
||||
"tslib": "^2.4.0",
|
||||
"tsx": "^3.12.3",
|
||||
"typescript": "4.8.4",
|
||||
"vh-check": "^2.0.5",
|
||||
"virtualizedtableforantd4": "1.3.0",
|
||||
"webpack": "^5.70.0",
|
||||
"yorkie": "^2.0.0"
|
||||
}
|
||||
|
||||
Generated
+16277
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB |
+57
-13
@@ -1,9 +1,3 @@
|
||||
## Version: v2.8.0
|
||||
## Date: 2021-06-20
|
||||
## Update Content: 可持续发展纲要\n1. session管理破坏性修改\n2. 配置管理可编辑config下文件\n3. 自定义脚本改为查看脚本\n4. 移除互助相关
|
||||
|
||||
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
|
||||
|
||||
## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务
|
||||
AutoDelCron="true"
|
||||
|
||||
@@ -16,19 +10,29 @@ DefaultCronRule=""
|
||||
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
|
||||
RepoFileExtensions="js py"
|
||||
|
||||
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
|
||||
## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890
|
||||
ProxyUrl=""
|
||||
|
||||
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||
CommandTimeoutTime="1h"
|
||||
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
|
||||
CpuWarn=80
|
||||
MemoryWarn=80
|
||||
DiskWarn=90
|
||||
|
||||
## 设置批量执行任务时的并发数,默认同时执行5个任务
|
||||
MaxConcurrentNum="5"
|
||||
## 设置定时任务执行的超时时间,例如1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||
CommandTimeoutTime=""
|
||||
|
||||
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
|
||||
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
|
||||
RandomDelay="300"
|
||||
|
||||
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
|
||||
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
|
||||
RandomDelayFileExtensions="js"
|
||||
|
||||
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
|
||||
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
|
||||
RandomDelayIgnoredMinutes="0 30"
|
||||
|
||||
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
|
||||
EnableExtraShell="true"
|
||||
|
||||
@@ -38,9 +42,12 @@ AutoStartBot=""
|
||||
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
|
||||
BotRepoUrl=""
|
||||
|
||||
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
||||
## 安装python依赖时指定pip源
|
||||
PipMirror="https://pypi.doubanio.com/simple/"
|
||||
|
||||
## 安装node依赖时指定npm源
|
||||
NpmMirror="https://registry.npmmirror.com"
|
||||
|
||||
## 通知环境变量
|
||||
## 1. Server酱
|
||||
## https://sct.ftqq.com
|
||||
@@ -51,7 +58,7 @@ export PUSH_KEY=""
|
||||
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
|
||||
export BARK_PUSH=""
|
||||
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
|
||||
export BARK_ICON="http://qn.whyour.cn/logo.png"
|
||||
export BARK_ICON="https://qn.whyour.cn/logo.png"
|
||||
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
|
||||
export BARK_SOUND=""
|
||||
## 下方填写推送消息分组,默认为"QingLong"
|
||||
@@ -84,6 +91,10 @@ export TG_API_HOST=""
|
||||
export DD_BOT_TOKEN=""
|
||||
export DD_BOT_SECRET=""
|
||||
|
||||
## 企业微信反向代理地址
|
||||
## (环境变量名 QYWX_ORIGIN)
|
||||
export QYWX_ORIGIN=""
|
||||
|
||||
## 5. 企业微信机器人
|
||||
## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
|
||||
## 下方填写密钥,企业微信推送 webhook 后面的 key
|
||||
@@ -129,4 +140,37 @@ export GOTIFY_PRIORITY=0
|
||||
## deer_key 填写PushDeer的key
|
||||
export DEER_KEY=""
|
||||
|
||||
## 12. Chat
|
||||
## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token=
|
||||
## chat_token 填写后面的token
|
||||
export CHAT_URL=""
|
||||
export CHAT_TOKEN=""
|
||||
|
||||
## 13. aibotk
|
||||
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
|
||||
## aibotk_key (必填)填写智能微秘书个人中心的apikey
|
||||
export AIBOTK_KEY=""
|
||||
## aibotk_type (必填)填写发送的目标 room 或 contact, 填其他的不生效
|
||||
export AIBOTK_TYPE=""
|
||||
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
|
||||
export AIBOTK_NAME=""
|
||||
|
||||
## 14. SMTP
|
||||
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
|
||||
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||
export SMTP_SERVER=""
|
||||
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
|
||||
export SMTP_SSL="false"
|
||||
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
||||
export SMTP_EMAIL=""
|
||||
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||
export SMTP_PASSWORD=""
|
||||
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
|
||||
export SMTP_NAME=""
|
||||
|
||||
## 15. PushMe
|
||||
## 官方说明文档:https://push.i-i.me/
|
||||
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
|
||||
export PUSHME_KEY=""
|
||||
|
||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||
|
||||
+368
-135
File diff suppressed because one or more lines are too long
+208
-21
@@ -9,6 +9,10 @@ import re
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.header import Header
|
||||
from email.utils import formataddr
|
||||
|
||||
import requests
|
||||
|
||||
@@ -61,13 +65,19 @@ push_config = {
|
||||
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
||||
|
||||
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
|
||||
|
||||
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
|
||||
|
||||
'CHAT_URL': '', # synology chat url
|
||||
'CHAT_TOKEN': '', # synology chat token
|
||||
|
||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||
|
||||
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
|
||||
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
||||
|
||||
'QYWX_ORIGIN': '', # 企业微信代理地址
|
||||
|
||||
'QYWX_AM': '', # 企业微信应用
|
||||
|
||||
'QYWX_KEY': '', # 企业微信机器人
|
||||
@@ -78,6 +88,18 @@ push_config = {
|
||||
'TG_PROXY_AUTH': '', # tg 代理认证参数
|
||||
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
|
||||
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
|
||||
|
||||
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
|
||||
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
|
||||
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
|
||||
|
||||
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
|
||||
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
|
||||
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
||||
|
||||
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
|
||||
}
|
||||
notify_function = []
|
||||
# fmt: on
|
||||
@@ -202,7 +224,7 @@ def go_cqhttp(title: str, content: str) -> None:
|
||||
print("go-cqhttp 推送失败!")
|
||||
|
||||
|
||||
def gotify(title:str,content:str) -> None:
|
||||
def gotify(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 gotify 推送消息。
|
||||
"""
|
||||
@@ -212,8 +234,12 @@ def gotify(title:str,content:str) -> None:
|
||||
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()
|
||||
data = {
|
||||
"title": title,
|
||||
"message": content,
|
||||
"priority": push_config.get("GOTIFY_PRIORITY"),
|
||||
}
|
||||
response = requests.post(url, data=data).json()
|
||||
|
||||
if response.get("id"):
|
||||
print("gotify 推送成功!")
|
||||
@@ -251,10 +277,10 @@ def serverJ(title: str, content: str) -> None:
|
||||
print("serverJ 服务启动")
|
||||
|
||||
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
||||
if push_config.get("PUSH_KEY").index("SCT") != -1:
|
||||
if push_config.get("PUSH_KEY").find("SCT") != -1:
|
||||
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||
else:
|
||||
url = f'https://sc.ftqq.com/${push_config.get("PUSH_KEY")}.send'
|
||||
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:
|
||||
@@ -271,15 +297,41 @@ def pushdeer(title: str, content: str) -> None:
|
||||
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("PushDeer 服务启动")
|
||||
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
|
||||
url = 'https://api2.pushdeer.com/message/push'
|
||||
data = {
|
||||
"text": title,
|
||||
"desp": content,
|
||||
"type": "markdown",
|
||||
"pushkey": push_config.get("DEER_KEY"),
|
||||
}
|
||||
url = "https://api2.pushdeer.com/message/push"
|
||||
if push_config.get("DEER_URL"):
|
||||
url = push_config.get("DEER_URL")
|
||||
|
||||
response = requests.post(url, data=data).json()
|
||||
|
||||
|
||||
if len(response.get("content").get("result")) > 0:
|
||||
print("PushDeer 推送成功!")
|
||||
else:
|
||||
print("PushDeer 推送失败!错误信息:", response)
|
||||
|
||||
|
||||
|
||||
def chat(title: str, content: str) -> None:
|
||||
"""
|
||||
通过Chat 推送消息
|
||||
"""
|
||||
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
|
||||
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("chat 服务启动")
|
||||
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
||||
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
|
||||
response = requests.post(url, data=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Chat 推送成功!")
|
||||
else:
|
||||
print("Chat 推送失败!错误信息:", response)
|
||||
|
||||
|
||||
def pushplus_bot(title: str, content: str) -> None:
|
||||
"""
|
||||
@@ -305,7 +357,6 @@ def pushplus_bot(title: str, content: str) -> None:
|
||||
print("PUSHPLUS 推送成功!")
|
||||
|
||||
else:
|
||||
|
||||
url_old = "http://pushplus.hxtrip.com/send"
|
||||
headers["Accept"] = "application/json"
|
||||
response = requests.post(url=url_old, data=body, headers=headers).json()
|
||||
@@ -376,9 +427,12 @@ class WeCom:
|
||||
self.CORPID = corpid
|
||||
self.CORPSECRET = corpsecret
|
||||
self.AGENTID = agentid
|
||||
self.ORIGIN = "https://qyapi.weixin.qq.com"
|
||||
if push_config.get("QYWX_ORIGIN"):
|
||||
self.ORIGIN = push_config.get("QYWX_ORIGIN")
|
||||
|
||||
def get_access_token(self):
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||
url = f"{self.ORIGIN}/cgi-bin/gettoken"
|
||||
values = {
|
||||
"corpid": self.CORPID,
|
||||
"corpsecret": self.CORPSECRET,
|
||||
@@ -388,10 +442,7 @@ class WeCom:
|
||||
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_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||
send_values = {
|
||||
"touser": touser,
|
||||
"msgtype": "text",
|
||||
@@ -405,10 +456,7 @@ class WeCom:
|
||||
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_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||
send_values = {
|
||||
"touser": touser,
|
||||
"msgtype": "mpnews",
|
||||
@@ -441,7 +489,11 @@ def wecom_bot(title: str, content: str) -> None:
|
||||
return
|
||||
print("企业微信机器人服务启动")
|
||||
|
||||
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
|
||||
origin = "https://qyapi.weixin.qq.com"
|
||||
if push_config.get("QYWX_ORIGIN"):
|
||||
origin = push_config.get("QYWX_ORIGIN")
|
||||
|
||||
url = f"{origin}/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(
|
||||
@@ -499,6 +551,116 @@ def telegram_bot(title: str, content: str) -> None:
|
||||
print("tg 推送失败!")
|
||||
|
||||
|
||||
def aibotk(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 智能微秘书 推送消息。
|
||||
"""
|
||||
if (
|
||||
not push_config.get("AIBOTK_KEY")
|
||||
or not push_config.get("AIBOTK_TYPE")
|
||||
or not push_config.get("AIBOTK_NAME")
|
||||
):
|
||||
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
|
||||
return
|
||||
print("智能微秘书 服务启动")
|
||||
|
||||
if push_config.get("AIBOTK_TYPE") == "room":
|
||||
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
|
||||
data = {
|
||||
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||
"roomName": push_config.get("AIBOTK_NAME"),
|
||||
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
|
||||
}
|
||||
else:
|
||||
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
|
||||
data = {
|
||||
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||
"name": push_config.get("AIBOTK_NAME"),
|
||||
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
|
||||
}
|
||||
body = json.dumps(data).encode(encoding="utf-8")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(url=url, data=body, headers=headers).json()
|
||||
print(response)
|
||||
if response["code"] == 0:
|
||||
print("智能微秘书 推送成功!")
|
||||
else:
|
||||
print(f'智能微秘书 推送失败!{response["error"]}')
|
||||
|
||||
|
||||
def smtp(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 SMTP 邮件 推送消息。
|
||||
"""
|
||||
if (
|
||||
not push_config.get("SMTP_SERVER")
|
||||
or not push_config.get("SMTP_SSL")
|
||||
or not push_config.get("SMTP_EMAIL")
|
||||
or not push_config.get("SMTP_PASSWORD")
|
||||
or not push_config.get("SMTP_NAME")
|
||||
):
|
||||
print(
|
||||
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
|
||||
)
|
||||
return
|
||||
print("SMTP 邮件 服务启动")
|
||||
|
||||
message = MIMEText(content, "plain", "utf-8")
|
||||
message["From"] = formataddr(
|
||||
(
|
||||
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
)
|
||||
)
|
||||
message["To"] = formataddr(
|
||||
(
|
||||
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
)
|
||||
)
|
||||
message["Subject"] = Header(title, "utf-8")
|
||||
|
||||
try:
|
||||
smtp_server = (
|
||||
smtplib.SMTP_SSL(push_config.get("SMTP_SERVER"))
|
||||
if push_config.get("SMTP_SSL") == "true"
|
||||
else smtplib.SMTP(push_config.get("SMTP_SERVER"))
|
||||
)
|
||||
smtp_server.login(
|
||||
push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD")
|
||||
)
|
||||
smtp_server.sendmail(
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
message.as_bytes(),
|
||||
)
|
||||
smtp_server.close()
|
||||
print("SMTP 邮件 推送成功!")
|
||||
except Exception as e:
|
||||
print(f"SMTP 邮件 推送失败!{e}")
|
||||
|
||||
def pushme(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 PushMe 推送消息。
|
||||
"""
|
||||
if not push_config.get("PUSHME_KEY"):
|
||||
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("PushMe 服务启动")
|
||||
|
||||
url = f'https://push.i-i.me/?push_key={push_config.get("PUSHME_KEY")}'
|
||||
data = {
|
||||
"title": title,
|
||||
"content": content,
|
||||
}
|
||||
response = requests.post(url, data=data)
|
||||
|
||||
if response.status_code == 200 and response.text == "success":
|
||||
print("PushMe 推送成功!")
|
||||
else:
|
||||
print(f"PushMe 推送失败!{response.status_code} {response.text}")
|
||||
|
||||
|
||||
def one() -> str:
|
||||
"""
|
||||
获取一条一言。
|
||||
@@ -527,6 +689,8 @@ if push_config.get("PUSH_KEY"):
|
||||
notify_function.append(serverJ)
|
||||
if push_config.get("DEER_KEY"):
|
||||
notify_function.append(pushdeer)
|
||||
if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"):
|
||||
notify_function.append(chat)
|
||||
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||
notify_function.append(pushplus_bot)
|
||||
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||
@@ -537,6 +701,22 @@ 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)
|
||||
if (
|
||||
push_config.get("AIBOTK_KEY")
|
||||
and push_config.get("AIBOTK_TYPE")
|
||||
and push_config.get("AIBOTK_NAME")
|
||||
):
|
||||
notify_function.append(aibotk)
|
||||
if (
|
||||
push_config.get("SMTP_SERVER")
|
||||
and push_config.get("SMTP_SSL")
|
||||
and push_config.get("SMTP_EMAIL")
|
||||
and push_config.get("SMTP_PASSWORD")
|
||||
and push_config.get("SMTP_NAME")
|
||||
):
|
||||
notify_function.append(smtp)
|
||||
if push_config.get("PUSHME_KEY"):
|
||||
notify_function.append(pushme)
|
||||
|
||||
|
||||
def send(title: str, content: str) -> None:
|
||||
@@ -544,6 +724,13 @@ def send(title: str, content: str) -> None:
|
||||
print(f"{title} 推送内容为空!")
|
||||
return
|
||||
|
||||
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
|
||||
skipTitle = os.getenv("SKIP_PUSH_TITLE")
|
||||
if skipTitle:
|
||||
if title in re.split("\n", skipTitle):
|
||||
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
|
||||
return
|
||||
|
||||
hitokoto = push_config.get("HITOKOTO")
|
||||
|
||||
text = one() if hitokoto else ""
|
||||
|
||||
+2
-6
@@ -3,14 +3,10 @@
|
||||
"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",
|
||||
"nodemailer": "^6.8.0",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"tunnel": "0.0.6",
|
||||
"ws": "^7.4.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,14 +5,14 @@ const envFound = dotenv.config();
|
||||
const accessKey = process.env.QINIU_AK;
|
||||
const secretKey = process.env.QINIU_SK;
|
||||
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
|
||||
const key = 'version.ts';
|
||||
const key = 'version.yaml';
|
||||
const options = {
|
||||
scope: `${process.env.QINIU_SCOPE}:${key}`,
|
||||
};
|
||||
const putPolicy = new qiniu.rs.PutPolicy(options);
|
||||
const uploadToken = putPolicy.uploadToken(mac);
|
||||
|
||||
const localFile = 'src/version.ts';
|
||||
const localFile = 'version.yaml';
|
||||
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
|
||||
const formUploader = new qiniu.form_up.FormUploader(config);
|
||||
const putExtra = new qiniu.form_up.PutExtra(
|
||||
|
||||
+206
-159
@@ -1,187 +1,234 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
create_token() {
|
||||
local token_command="tsx ${dir_root}/back/token.ts"
|
||||
local token_file="${dir_root}/static/build/token.js"
|
||||
if [[ -f $token_file ]]; then
|
||||
token_command="node ${token_file}"
|
||||
fi
|
||||
token=$(eval "$token_command")
|
||||
}
|
||||
|
||||
get_token() {
|
||||
if [[ -f $file_auth_token ]]; then
|
||||
token=$(cat $file_auth_token | jq -r .value)
|
||||
local expiration=$(cat $file_auth_token | jq -r .expiration)
|
||||
local currentTimeStamp=$(date +%s)
|
||||
if [[ $currentTimeStamp -ge $expiration ]]; then
|
||||
create_token
|
||||
fi
|
||||
else
|
||||
create_token
|
||||
fi
|
||||
}
|
||||
|
||||
add_cron_api() {
|
||||
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}')
|
||||
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
|
||||
else
|
||||
local schedule="$1"
|
||||
local command="$2"
|
||||
local name="$3"
|
||||
local sub_id="$4"
|
||||
fi
|
||||
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/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
|
||||
if [[ ! $sub_id ]]; then
|
||||
sub_id="null"
|
||||
fi
|
||||
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/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\",\"sub_id\":$sub_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_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 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/open/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
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/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 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/open/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
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/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 ids=$1
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/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
|
||||
local ids="$1"
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/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
|
||||
}
|
||||
|
||||
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/open/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" >> $dir_log/$log_path
|
||||
fi
|
||||
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/open/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"
|
||||
fi
|
||||
}
|
||||
|
||||
notify_api() {
|
||||
local title=$1
|
||||
local content=$2
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
|
||||
-X 'PUT' \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo $api | jq -r .code)
|
||||
message=$(echo $api | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "通知发送成功"
|
||||
else
|
||||
echo -e "通知失败(${message})"
|
||||
fi
|
||||
local title="$1"
|
||||
local content="$2"
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
|
||||
-X 'PUT' \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
message=$(echo "$api" | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "通知发送成功🎉"
|
||||
else
|
||||
echo -e "通知失败(${message})"
|
||||
fi
|
||||
}
|
||||
|
||||
find_cron_api() {
|
||||
local params="$1"
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--compressed
|
||||
)
|
||||
data=$(echo "$api" | jq -r .data)
|
||||
if [[ $data == 'null' ]]; then
|
||||
echo -e ""
|
||||
else
|
||||
name=$(echo "$api" | jq -r .data.name)
|
||||
echo -e "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
get_token
|
||||
|
||||
Regular → Executable
+5
-10
@@ -1,9 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 导入通用变量与函数
|
||||
dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
|
||||
if [[ -z ${BotRepoUrl} ]]; then
|
||||
url="https://github.com/SuMaiKaDe/bot.git"
|
||||
repo_path="${dir_repo}/dockerbot"
|
||||
@@ -35,11 +31,10 @@ fi
|
||||
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
|
||||
|
||||
cd $dir_data
|
||||
cat requirements.txt | while read LREAD
|
||||
do
|
||||
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
|
||||
pip3 --default-timeout=100 install ${LREAD}
|
||||
fi
|
||||
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"
|
||||
@@ -47,6 +42,6 @@ echo -e "\npython3依赖安装成功...\n"
|
||||
echo -e "4、启动bot程序...\n"
|
||||
make_dir $dir_log/bot
|
||||
cd $dir_data
|
||||
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
|
||||
ps -eo pid,command | 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"
|
||||
|
||||
Regular → Executable
+18
-19
@@ -1,9 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
reset_env() {
|
||||
echo -e "---> 1. 开始检测配置文件\n"
|
||||
fix_config
|
||||
@@ -29,8 +25,7 @@ copy_dep() {
|
||||
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
|
||||
init_nginx
|
||||
echo -e "---> 配置文件复制完成\n"
|
||||
}
|
||||
|
||||
@@ -38,12 +33,12 @@ pm2_log() {
|
||||
echo -e "---> pm2日志"
|
||||
local panelOut="/root/.pm2/logs/panel-out.log"
|
||||
local panelError="/root/.pm2/logs/panel-error.log"
|
||||
tail -n 100 "$panelOut"
|
||||
tail -n 100 "$panelError"
|
||||
tail -n 300 "$panelOut"
|
||||
tail -n 300 "$panelError"
|
||||
}
|
||||
|
||||
check_nginx() {
|
||||
local nginxPid=$(ps -ef | grep nginx | grep -v grep)
|
||||
local nginxPid=$(ps -eo pid,command | grep nginx | grep -v grep)
|
||||
echo -e "=====> 检测nginx服务\n$nginxPid"
|
||||
if [[ $nginxPid ]]; then
|
||||
echo -e "\n=====> nginx服务正常\n"
|
||||
@@ -59,9 +54,6 @@ check_ql() {
|
||||
echo -e "\n=====> 检测面板\n\n$api\n"
|
||||
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
|
||||
echo -e "=====> 面板服务启动正常\n"
|
||||
else
|
||||
echo -e "=====> 面板服务异常,重置基础环境\n"
|
||||
reset_env
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -69,9 +61,8 @@ check_pm2() {
|
||||
pm2_log
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/system?t=$currentTimeStamp" \
|
||||
-H 'Accept: */*' \
|
||||
-H "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' \
|
||||
@@ -80,16 +71,24 @@ check_pm2() {
|
||||
echo -e "\n=====> 检测后台\n\n$api\n"
|
||||
if [[ $api =~ "{\"code\"" ]]; then
|
||||
echo -e "=====> 后台服务启动正常\n"
|
||||
else
|
||||
echo -e "=====> 后台服务异常,重置基础环境并重启后台\n"
|
||||
reset_env
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
echo -e "=====> 开始检测"
|
||||
npm i -g pnpm
|
||||
pnpm add -g pm2
|
||||
npm i -g pnpm@8.3.1
|
||||
patch_version
|
||||
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
if [[ $NpmMirror ]]; then
|
||||
cd && pnpm config set registry $NpmMirror
|
||||
pnpm install -g
|
||||
fi
|
||||
|
||||
pnpm add -g pm2 tsx
|
||||
reset_env
|
||||
copy_dep
|
||||
check_ql
|
||||
check_nginx
|
||||
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
random_delay() {
|
||||
local random_delay_max=$RandomDelay
|
||||
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
|
||||
local file_param=$1
|
||||
local file_extensions=${RandomDelayFileExtensions-"js"}
|
||||
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
|
||||
|
||||
if [[ -n $file_extensions ]]; then
|
||||
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
|
||||
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
local current_min
|
||||
current_min=$(date "+%-M")
|
||||
for minute in $ignored_minutes; do
|
||||
if [[ $current_min -eq $minute ]]; then
|
||||
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
|
||||
echo -e "任务随机延迟 $delay_second 秒,配置文件参数 RandomDelay 置空可取消延迟 \n"
|
||||
sleep $delay_second
|
||||
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}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
|
||||
fi
|
||||
done
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
## 使用说明
|
||||
usage() {
|
||||
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 &
|
||||
}
|
||||
|
||||
check_server() {
|
||||
if [[ $cpu_warn ]] && [[ $mem_warn ]] && [[ $disk_warn ]]; then
|
||||
local top_result=$(top -b -n 1)
|
||||
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
|
||||
|
||||
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
|
||||
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
|
||||
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
|
||||
|
||||
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
|
||||
if [[ $cpu_use -gt $cpu_warn ]] && [[ $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
|
||||
local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
|
||||
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
env_str_to_array() {
|
||||
local IFS="&"
|
||||
read -ra array <<<"${!env_param}"
|
||||
}
|
||||
|
||||
## 正常运行单个脚本,$1:传入参数
|
||||
run_normal() {
|
||||
local file_param=$1
|
||||
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]]; then
|
||||
random_delay "$file_param"
|
||||
fi
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ${file_param} != /* ]] && [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
|
||||
$timeoutCmd $which_program $file_param
|
||||
}
|
||||
|
||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||
run_concurrent() {
|
||||
local file_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
|
||||
|
||||
env_str_to_array
|
||||
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | 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=$(
|
||||
IFS="&"
|
||||
echo "${array_run[*]}"
|
||||
)
|
||||
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
|
||||
|
||||
env_str_to_array
|
||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
for i in "${!array[@]}"; do
|
||||
export "${env_param}=${array[i]}"
|
||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
eval $timeoutCmd $which_program $file_param &>$single_log_path &
|
||||
done
|
||||
|
||||
wait
|
||||
for i in "${!array[@]}"; do
|
||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
cat $single_log_path
|
||||
[[ -f $single_log_path ]] && rm -f $single_log_path
|
||||
done
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
env_str_to_array
|
||||
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | 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=$(
|
||||
IFS="&"
|
||||
echo "${array_run[*]}"
|
||||
)
|
||||
[[ ! -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
|
||||
$timeoutCmd $which_program $file_param
|
||||
}
|
||||
|
||||
## 运行其他命令
|
||||
run_else() {
|
||||
local file_param="$1"
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//.\/}
|
||||
fi
|
||||
|
||||
shift
|
||||
|
||||
$timeoutCmd $which_program $file_param "$@"
|
||||
}
|
||||
|
||||
## 命令检测
|
||||
main() {
|
||||
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $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
|
||||
elif [[ $# -eq 0 ]]; then
|
||||
echo
|
||||
usage
|
||||
else
|
||||
run_else "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
handle_task_before "$@"
|
||||
main "$@"
|
||||
handle_task_after "$@"
|
||||
Regular → Executable
+3
-3
@@ -11,9 +11,9 @@ echo -e "提交master代码"
|
||||
git push
|
||||
|
||||
echo -e "更新cdn文件"
|
||||
ts-node sample/tool.ts
|
||||
tsx sample/tool.ts
|
||||
|
||||
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*")
|
||||
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
|
||||
version="v$string"
|
||||
echo -e "当前版本$version"
|
||||
|
||||
@@ -29,4 +29,4 @@ git tag -a "$version" -m "release $version"
|
||||
echo -e "提交tag"
|
||||
git push --tags
|
||||
|
||||
echo -e "完成发布"
|
||||
echo -e "完成发布"
|
||||
|
||||
+33
-27
@@ -1,42 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 导入通用变量与函数
|
||||
dir_shell=$QL_DIR/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
|
||||
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
|
||||
if [[ $diff_time -gt $((${days} * 86400)) ]]; then
|
||||
local log_path=$(echo "$log" | sed "s,${dir_log}/,,g")
|
||||
local result=$(find_cron_api "log_path=$log_path")
|
||||
echo -e "查询文件 $log_path"
|
||||
if [[ -z $result ]]; then
|
||||
echo -e "删除中~"
|
||||
rm -vf $log
|
||||
else
|
||||
echo -e "正在被 $result 使用,跳过~"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
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
|
||||
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"
|
||||
echo -e "查找旧日志文件中...\n"
|
||||
remove_js_log
|
||||
remove_empty_dir
|
||||
echo -e "删除旧日志执行完毕\n"
|
||||
fi
|
||||
|
||||
+307
-283
@@ -2,6 +2,7 @@
|
||||
|
||||
## 目录
|
||||
dir_root=$QL_DIR
|
||||
dir_tmp=$dir_root/.tmp
|
||||
dir_data=$dir_root/data
|
||||
dir_shell=$dir_root/shell
|
||||
dir_sample=$dir_root/sample
|
||||
@@ -18,6 +19,7 @@ dir_update_log=$dir_log/update
|
||||
ql_static_repo=$dir_repo/static
|
||||
|
||||
## 文件
|
||||
file_ecosystem_js=$dir_root/ecosystem.config.js
|
||||
file_config_sample=$dir_sample/config.sample.sh
|
||||
file_env=$dir_config/env.sh
|
||||
file_sharecode=$dir_config/sharecode.sh
|
||||
@@ -34,7 +36,6 @@ 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
|
||||
dep_notify_py=$dir_dep/notify.py
|
||||
@@ -50,384 +51,407 @@ list_own_drop=$dir_list_tmp/own_drop.list
|
||||
|
||||
## 软连接及其原始文件对应关系
|
||||
link_name=(
|
||||
task
|
||||
ql
|
||||
task
|
||||
ql
|
||||
)
|
||||
original_name=(
|
||||
task.sh
|
||||
update.sh
|
||||
task.sh
|
||||
update.sh
|
||||
)
|
||||
|
||||
init_env() {
|
||||
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
|
||||
export PYTHONUNBUFFERED=1
|
||||
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
|
||||
export PYTHONUNBUFFERED=1
|
||||
}
|
||||
|
||||
import_config() {
|
||||
[[ -f $file_config_user ]] && . $file_config_user
|
||||
[[ -f $file_env ]] && . $file_env
|
||||
[[ -f $file_config_user ]] && . $file_config_user
|
||||
[[ -f $file_env ]] && . $file_env
|
||||
|
||||
command_timeout_time=${CommandTimeoutTime:-"1h"}
|
||||
proxy_url=${ProxyUrl:-""}
|
||||
file_extensions=${RepoFileExtensions:-"js py"}
|
||||
current_branch=${QL_BRANCH}
|
||||
ql_base_url=${QlBaseUrl:-"/"}
|
||||
command_timeout_time=${CommandTimeoutTime:-""}
|
||||
proxy_url=${ProxyUrl:-""}
|
||||
file_extensions=${RepoFileExtensions:-"js py"}
|
||||
current_branch=${QL_BRANCH}
|
||||
|
||||
if [[ -n "${DefaultCronRule}" ]]; then
|
||||
default_cron="${DefaultCronRule}"
|
||||
else
|
||||
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
||||
fi
|
||||
if [[ -n "${DefaultCronRule}" ]]; then
|
||||
default_cron="${DefaultCronRule}"
|
||||
else
|
||||
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
||||
fi
|
||||
|
||||
cpu_warn=${CpuWarn}
|
||||
mem_warn=${MemoryWarn}
|
||||
disk_warn=${DiskWarn}
|
||||
}
|
||||
|
||||
set_proxy() {
|
||||
if [[ $proxy_url ]]; then
|
||||
export http_proxy="${proxy_url}"
|
||||
export https_proxy="${proxy_url}"
|
||||
fi
|
||||
local proxy="$1"
|
||||
if [[ $proxy ]]; then
|
||||
proxy_url="$proxy"
|
||||
fi
|
||||
if [[ $proxy_url ]]; then
|
||||
export http_proxy="${proxy_url}"
|
||||
export https_proxy="${proxy_url}"
|
||||
fi
|
||||
}
|
||||
|
||||
unset_proxy() {
|
||||
unset http_proxy
|
||||
unset https_proxy
|
||||
unset http_proxy
|
||||
unset https_proxy
|
||||
unset ftp_proxy
|
||||
unset all_proxy
|
||||
unset no_proxy
|
||||
}
|
||||
|
||||
make_dir() {
|
||||
local dir=$1
|
||||
if [[ ! -d $dir ]]; then
|
||||
mkdir -p $dir
|
||||
fi
|
||||
local dir=$1
|
||||
if [[ ! -d $dir ]]; then
|
||||
mkdir -p $dir
|
||||
fi
|
||||
}
|
||||
|
||||
detect_termux() {
|
||||
if [[ $PATH == *com.termux* ]]; then
|
||||
is_termux=1
|
||||
else
|
||||
is_termux=0
|
||||
fi
|
||||
if [[ $PATH == *com.termux* ]]; then
|
||||
is_termux=1
|
||||
else
|
||||
is_termux=0
|
||||
fi
|
||||
}
|
||||
|
||||
detect_macos() {
|
||||
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
|
||||
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
|
||||
}
|
||||
|
||||
gen_random_num() {
|
||||
local divi=$1
|
||||
echo $((${RANDOM} % $divi))
|
||||
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
|
||||
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
|
||||
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
|
||||
local cmd_prefix cmd_suffix
|
||||
if type task &>/dev/null; then
|
||||
cmd_suffix=""
|
||||
if [[ -f "$dir_shell/task.sh" ]]; then
|
||||
cmd_prefix=""
|
||||
else
|
||||
cmd_suffix=".sh"
|
||||
if [[ -f "$dir_shell/task.sh" ]]; then
|
||||
cmd_prefix="$dir_shell/"
|
||||
else
|
||||
cmd_prefix="bash $dir_shell/"
|
||||
fi
|
||||
cmd_prefix="bash "
|
||||
fi
|
||||
for ((i = 0; i < ${#link_name[*]}; i++)); do
|
||||
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
|
||||
done
|
||||
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_static
|
||||
make_dir $dir_data
|
||||
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
|
||||
make_dir $dir_tmp
|
||||
make_dir $dir_static
|
||||
make_dir $dir_data
|
||||
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 [[ ! -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_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_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_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 [[ ! -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_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_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 $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
|
||||
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
|
||||
echo -e "检测到默认nginx配置文件,清空...\n"
|
||||
cat /dev/null >/etc/nginx/conf.d/default.conf
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $dep_notify_js ]]; then
|
||||
echo -e "复制一份 $file_notify_js_sample 为 $dep_notify_js\n"
|
||||
cp -fv $file_notify_js_sample $dep_notify_js
|
||||
echo
|
||||
fi
|
||||
if [[ ! -s $dep_notify_js ]]; then
|
||||
echo -e "复制一份 $file_notify_js_sample 为 $dep_notify_js\n"
|
||||
cp -fv $file_notify_js_sample $dep_notify_js
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $dep_notify_py ]]; then
|
||||
echo -e "复制一份 $file_notify_py_sample 为 $dep_notify_py\n"
|
||||
cp -fv $file_notify_py_sample $dep_notify_py
|
||||
echo
|
||||
fi
|
||||
if [[ ! -s $dep_notify_py ]]; then
|
||||
echo -e "复制一份 $file_notify_py_sample 为 $dep_notify_py\n"
|
||||
cp -fv $file_notify_py_sample $dep_notify_py
|
||||
echo
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
npm_install_sub() {
|
||||
set_proxy
|
||||
if [ $is_termux -eq 1 ]; then
|
||||
npm install --production --no-bin-links --registry=https://registry.npmmirror.com || npm install --production --no-bin-links
|
||||
elif ! type pnpm &>/dev/null; then
|
||||
npm install --production --registry=https://registry.npmmirror.com || npm install --production
|
||||
else
|
||||
pnpm install --loglevel error --production --registry=https://registry.npmmirror.com || pnpm install --production --loglevel error
|
||||
fi
|
||||
unset_proxy
|
||||
}
|
||||
|
||||
npm_install_1() {
|
||||
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
|
||||
if [ $is_termux -eq 1 ]; then
|
||||
npm install --production --no-bin-links
|
||||
elif ! type pnpm &>/dev/null; then
|
||||
npm install --production
|
||||
else
|
||||
pnpm install --loglevel error --production
|
||||
fi
|
||||
exit_status=$?
|
||||
}
|
||||
|
||||
npm_install_2() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work=$1
|
||||
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
|
||||
cd $dir_work
|
||||
echo -e "安装 $dir_work 依赖包...\n"
|
||||
npm_install_sub
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
git_clone_scripts() {
|
||||
local url=$1
|
||||
local dir=$2
|
||||
local branch=$3
|
||||
[[ $branch ]] && local part_cmd="-b $branch "
|
||||
echo -e "开始克隆仓库 $url 到 $dir\n"
|
||||
local url="$1"
|
||||
local dir="$2"
|
||||
local branch="$3"
|
||||
local proxy="$4"
|
||||
[[ $branch ]] && local part_cmd="-b $branch "
|
||||
echo -e "开始拉取仓库 ${uniq_path} 到 $dir\n"
|
||||
|
||||
set_proxy
|
||||
git clone $part_cmd $url $dir
|
||||
exit_status=$?
|
||||
unset_proxy
|
||||
}
|
||||
set_proxy "$proxy"
|
||||
|
||||
git_pull_scripts() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work="$1"
|
||||
local branch="$2"
|
||||
cd $dir_work
|
||||
echo -e "开始更新仓库:$dir_work\n"
|
||||
git clone --depth=1 $part_cmd $url $dir 2>&1
|
||||
exit_status=$?
|
||||
|
||||
set_proxy
|
||||
git fetch --all
|
||||
exit_status=$?
|
||||
git pull &>/dev/null
|
||||
unset_proxy
|
||||
reset_branch "$branch"
|
||||
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
reset_romote_url() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work=$1
|
||||
local url=$2
|
||||
local branch="$3"
|
||||
|
||||
if [[ -d "$dir_work/.git" ]]; then
|
||||
cd $dir_work
|
||||
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
||||
git remote set-url origin $url &>/dev/null
|
||||
|
||||
local part_cmd=""
|
||||
reset_branch "$branch"
|
||||
cd $dir_current
|
||||
fi
|
||||
}
|
||||
|
||||
reset_branch() {
|
||||
local branch="$1"
|
||||
if [[ $branch ]]; then
|
||||
part_cmd="origin/${branch}"
|
||||
git checkout -B "$branch"
|
||||
git branch --set-upstream-to=$part_cmd $branch
|
||||
fi
|
||||
git reset --hard $part_cmd &>/dev/null
|
||||
unset_proxy
|
||||
}
|
||||
|
||||
random_range() {
|
||||
local beg=$1
|
||||
local end=$2
|
||||
echo $((RANDOM % ($end - $beg) + $beg))
|
||||
local beg=$1
|
||||
local end=$2
|
||||
echo $((RANDOM % ($end - $beg) + $beg))
|
||||
}
|
||||
|
||||
reload_pm2() {
|
||||
pm2 l &>/dev/null
|
||||
|
||||
echo -e "启动面板服务\n"
|
||||
pm2 delete panel --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||
|
||||
echo -e "启动定时任务服务\n"
|
||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||
|
||||
echo -e "启动公开服务\n"
|
||||
pm2 delete public --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
|
||||
cd $dir_root
|
||||
# 代理会影响 grpc 服务
|
||||
unset_proxy
|
||||
pm2 flush &>/dev/null
|
||||
pm2 startOrGracefulReload $file_ecosystem_js --update-env
|
||||
}
|
||||
|
||||
diff_time() {
|
||||
local format="$1"
|
||||
local begin_time="$2"
|
||||
local end_time="$3"
|
||||
local format="$1"
|
||||
local begin_time="$2"
|
||||
local end_time="$3"
|
||||
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
|
||||
else
|
||||
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
||||
fi
|
||||
echo "$diff_time"
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
|
||||
else
|
||||
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
||||
fi
|
||||
echo "$diff_time"
|
||||
}
|
||||
|
||||
format_time() {
|
||||
local format="$1"
|
||||
local time="$2"
|
||||
local format="$1"
|
||||
local time="$2"
|
||||
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
|
||||
else
|
||||
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
|
||||
fi
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
|
||||
else
|
||||
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
|
||||
fi
|
||||
}
|
||||
|
||||
format_log_time() {
|
||||
local format="$1"
|
||||
local time="$2"
|
||||
local format="$1"
|
||||
local time="$2"
|
||||
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
|
||||
else
|
||||
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
|
||||
fi
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
|
||||
else
|
||||
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
|
||||
fi
|
||||
}
|
||||
|
||||
format_timestamp() {
|
||||
local format="$1"
|
||||
local time="$2"
|
||||
local format="$1"
|
||||
local time="$2"
|
||||
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
echo $(date -j -f "$format" "$time" "+%s")
|
||||
else
|
||||
echo $(date -d "$time" "+%s")
|
||||
fi
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
echo $(date -j -f "$format" "$time" "+%s")
|
||||
else
|
||||
echo $(date -d "$time" "+%s")
|
||||
fi
|
||||
}
|
||||
|
||||
patch_version() {
|
||||
git config --global pull.rebase false
|
||||
|
||||
cp -f $dir_root/.env.example $dir_root/.env
|
||||
|
||||
if [[ -f "$dir_root/db/cookie.db" ]]; then
|
||||
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
||||
mv $dir_root/db/cookie.db $dir_root/db/env.db
|
||||
rm -rf $dir_root/db/cookie.db
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ -d "$dir_root/db" ]]; then
|
||||
echo -e "检测到旧的db目录,拷贝到data目录...\n"
|
||||
cp -rf $dir_root/config $dir_root/data
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ -d "$dir_root/scripts" ]]; then
|
||||
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
|
||||
cp -rf $dir_root/scripts $dir_root/data
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ -d "$dir_root/log" ]]; then
|
||||
echo -e "检测到旧的log目录,拷贝到data目录...\n"
|
||||
cp -rf $dir_root/log $dir_root/data
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ -d "$dir_root/config" ]]; then
|
||||
echo -e "检测到旧的config目录,拷贝到data目录...\n"
|
||||
cp -rf $dir_root/config $dir_root/data
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
init_nginx() {
|
||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||
local location_url="/"
|
||||
local aliasStr=""
|
||||
local rootStr=""
|
||||
if [[ $ql_base_url != "/" ]]; then
|
||||
location_url="^~${ql_base_url%*/}"
|
||||
aliasStr="alias ${dir_static}/dist;"
|
||||
else
|
||||
rootStr="root ${dir_static}/dist;"
|
||||
fi
|
||||
sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf
|
||||
sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf
|
||||
sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf
|
||||
sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf
|
||||
|
||||
ipv6=$(ip a | grep inet6)
|
||||
ipv6Str=""
|
||||
if [[ $ipv6 ]]; then
|
||||
ipv6Str="listen [::]:5700 ipv6only=on;"
|
||||
fi
|
||||
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
|
||||
}
|
||||
|
||||
handle_task_before() {
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
|
||||
echo -e "## 开始执行... $begin_time\n"
|
||||
|
||||
[[ $is_macos -eq 0 ]] && check_server
|
||||
|
||||
. $file_task_before "$@"
|
||||
}
|
||||
|
||||
handle_task_after() {
|
||||
. $file_task_after "$@"
|
||||
|
||||
local etime=$(date "+$time_format")
|
||||
local end_time=$(format_time "$time_format" "$etime")
|
||||
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||
local diff_time=$(($end_timestamp - $begin_timestamp))
|
||||
|
||||
[[ "$diff_time" == 0 ]] && diff_time=1
|
||||
|
||||
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
||||
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
}
|
||||
|
||||
init_env
|
||||
detect_termux
|
||||
detect_macos
|
||||
define_cmd
|
||||
fix_config
|
||||
|
||||
import_config $1 >$task_error_log_path 2>&1
|
||||
import_config $1
|
||||
|
||||
+104
-367
@@ -5,388 +5,125 @@ dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
trap "single_hanle" 2 3 20 15 14
|
||||
single_hanle() {
|
||||
eval handle_task_after "$@" "$cmd"
|
||||
exit 1
|
||||
}
|
||||
|
||||
## 选择python3还是node
|
||||
define_program() {
|
||||
local file_param=$1
|
||||
if [[ $file_param == *.js ]]; then
|
||||
which_program="node"
|
||||
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
|
||||
which_program="python3"
|
||||
elif [[ $file_param == *.sh ]]; then
|
||||
which_program="bash"
|
||||
elif [[ $file_param == *.ts ]]; then
|
||||
which_program="ts-node-transpile-only"
|
||||
local file_param=$1
|
||||
if [[ $file_param == *.js ]] || [[ $file_param == *.mjs ]]; then
|
||||
which_program="node"
|
||||
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
|
||||
which_program="python3"
|
||||
elif [[ $file_param == *.sh ]]; then
|
||||
which_program="bash"
|
||||
elif [[ $file_param == *.ts ]]; then
|
||||
if ! type tsx &>/dev/null; then
|
||||
which_program="ts-node-transpile-only"
|
||||
else
|
||||
which_program=""
|
||||
which_program="tsx"
|
||||
fi
|
||||
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
|
||||
}
|
||||
handle_log_path() {
|
||||
local file_param=$1
|
||||
|
||||
## 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
|
||||
}
|
||||
if [[ -z $file_param ]]; then
|
||||
file_param="task"
|
||||
fi
|
||||
|
||||
## 使用说明
|
||||
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
|
||||
if [[ -z $ID ]]; then
|
||||
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}')
|
||||
fi
|
||||
local suffix=""
|
||||
if [[ ! -z $ID ]]; then
|
||||
if [[ "$ID" -gt 0 ]] 2>/dev/null; then
|
||||
suffix="_${ID}"
|
||||
else
|
||||
echo -e "\n暂无脚本可以执行"
|
||||
ID=""
|
||||
fi
|
||||
fi
|
||||
|
||||
time=$(date "+$mtime_format")
|
||||
log_time=$(format_log_time "$mtime_format" "$time")
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
if [[ $file_param =~ "/" ]]; then
|
||||
if [[ $file_param == /* ]]; then
|
||||
log_dir_tmp_path="${file_param:1}"
|
||||
else
|
||||
log_dir_tmp_path="${file_param}"
|
||||
fi
|
||||
fi
|
||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="${log_dir_tmp%.*}${suffix}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
|
||||
if [[ $real_log_path ]]; then
|
||||
log_path="$real_log_path"
|
||||
fi
|
||||
|
||||
cmd=">> $dir_log/$log_path 2>&1"
|
||||
make_dir "$dir_log/$log_dir"
|
||||
if [[ "$show_log" == "true" ]]; then
|
||||
cmd="2>&1 | tee -a $dir_log/$log_path"
|
||||
fi
|
||||
|
||||
if [[ "$real_time" == "true" ]]; then
|
||||
cmd=""
|
||||
fi
|
||||
}
|
||||
|
||||
## run nohup,$1:文件名,不含路径,带后缀
|
||||
run_nohup() {
|
||||
local file_name=$1
|
||||
nohup node $file_name &>$log_path &
|
||||
}
|
||||
|
||||
## 正常运行单个脚本,$1:传入参数
|
||||
run_normal() {
|
||||
local file_param=$1
|
||||
define_program "$file_param"
|
||||
if [[ $file_param == *.js ]]; then
|
||||
if [[ $# -eq 1 ]]; then
|
||||
random_delay
|
||||
fi
|
||||
fi
|
||||
|
||||
local time=$(date "+$time_format")
|
||||
log_time=$(format_log_time "$time_format" "$time")
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
if [[ $file_param =~ "/" ]]; then
|
||||
if [[ $file_param == /* ]]; then
|
||||
log_dir_tmp_path="${file_param:1}"
|
||||
else
|
||||
log_dir_tmp_path="${file_param}"
|
||||
fi
|
||||
fi
|
||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="${log_dir_tmp%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd=">> $dir_log/$log_path 2>&1"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir "$dir_log/$log_dir"
|
||||
|
||||
local begin_time=$(format_time "$time_format" "$time")
|
||||
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
|
||||
eval $timeoutCmd $which_program $file_param $cmd
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s")
|
||||
local diff_time=$(expr $end_timestamp - $begin_timestamp)
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
}
|
||||
|
||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||
run_concurrent() {
|
||||
local file_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')
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
|
||||
define_program "$file_param"
|
||||
local time=$(date "+$time_format")
|
||||
log_time=$(format_log_time "$time_format" "$time")
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
if [[ $file_param =~ "/" ]]; then
|
||||
if [[ $file_param == /* ]]; then
|
||||
log_dir_tmp_path="${file_param:1}"
|
||||
else
|
||||
log_dir_tmp_path="${file_param}"
|
||||
fi
|
||||
fi
|
||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="${log_dir_tmp%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd=">> $dir_log/$log_path 2>&1"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir "$dir_log/$log_dir"
|
||||
|
||||
local begin_time=$(format_time "$time_format" "$time")
|
||||
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
for i in "${!array[@]}"; do
|
||||
export ${env_param}=${array[i]}
|
||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
eval $timeoutCmd $which_program $file_param &>$single_log_path &
|
||||
done
|
||||
|
||||
wait
|
||||
for i in "${!array[@]}"; do
|
||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
eval cat $single_log_path $cmd
|
||||
[[ -f $single_log_path ]] && rm -f $single_log_path
|
||||
done
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
}
|
||||
|
||||
run_designated() {
|
||||
local file_param="$1"
|
||||
local env_param="$2"
|
||||
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
||||
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
|
||||
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
define_program "$file_param"
|
||||
local time=$(date "+$time_format")
|
||||
log_time=$(format_log_time "$time_format" "$time")
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
if [[ $file_param =~ "/" ]]; then
|
||||
if [[ $file_param == /* ]]; then
|
||||
log_dir_tmp_path="${file_param:1}"
|
||||
else
|
||||
log_dir_tmp_path="${file_param}"
|
||||
fi
|
||||
fi
|
||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="${log_dir_tmp%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd=">> $dir_log/$log_path 2>&1"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir "$dir_log/$log_dir"
|
||||
|
||||
local begin_time=$(format_time "$time_format" "$time")
|
||||
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||
local runArr=($(eval echo $tempArr))
|
||||
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
|
||||
|
||||
local n=0
|
||||
for i in ${runArr[@]}; do
|
||||
array_run[n]=${array[$i - 1]}
|
||||
let n++
|
||||
done
|
||||
|
||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
eval $timeoutCmd $which_program $file_param $cmd
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
}
|
||||
|
||||
## 运行其他命令
|
||||
run_else() {
|
||||
local file_param="$1"
|
||||
define_program "$file_param"
|
||||
local time=$(date "+$time_format")
|
||||
log_time=$(format_log_time "$time_format" "$time")
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
if [[ $file_param =~ "/" ]]; then
|
||||
if [[ $file_param == /* ]]; then
|
||||
log_dir_tmp_path="${file_param:1}"
|
||||
else
|
||||
log_dir_tmp_path="${file_param}"
|
||||
fi
|
||||
fi
|
||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="${log_dir_tmp%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd=">> $dir_log/$log_path 2>&1"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir "$dir_log/$log_dir"
|
||||
|
||||
local begin_time=$(format_time "$time_format" "$time")
|
||||
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
|
||||
shift
|
||||
eval $timeoutCmd $which_program "$file_param" "$@" $cmd
|
||||
|
||||
eval . $file_task_after "$file_param" "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
}
|
||||
|
||||
## 命令检测
|
||||
main() {
|
||||
show_log="false"
|
||||
while getopts ":l" opt
|
||||
do
|
||||
case $opt in
|
||||
l)
|
||||
show_log="true"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
||||
|
||||
timeoutCmd=""
|
||||
format_params() {
|
||||
time_format="%Y-%m-%d %H:%M:%S"
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
mtime_format=$time_format
|
||||
else
|
||||
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
|
||||
fi
|
||||
timeoutCmd=""
|
||||
if [[ $command_timeout_time ]]; then
|
||||
if type timeout &>/dev/null; then
|
||||
timeoutCmd="timeout -k 10s $command_timeout_time "
|
||||
fi
|
||||
|
||||
time_format="%Y-%m-%d %H:%M:%S"
|
||||
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
|
||||
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 "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
|
||||
elif [[ $# -eq 0 ]]; then
|
||||
echo
|
||||
usage
|
||||
else
|
||||
run_else "$@"
|
||||
timeoutCmd="timeout --foreground -s 2 -k 10s $command_timeout_time "
|
||||
fi
|
||||
fi
|
||||
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
|
||||
}
|
||||
|
||||
main "$@"
|
||||
init_begin_time() {
|
||||
begin_time=$(format_time "$time_format" "$time")
|
||||
begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||
}
|
||||
|
||||
while getopts ":lm:" opt; do
|
||||
case $opt in
|
||||
l)
|
||||
show_log="true"
|
||||
;;
|
||||
m)
|
||||
max_time="$OPTARG"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ $show_log ]] && shift $(($OPTIND - 1))
|
||||
if [[ $max_time ]]; then
|
||||
shift $(($OPTIND - 1))
|
||||
command_timeout_time="$max_time"
|
||||
fi
|
||||
|
||||
format_params "$@"
|
||||
define_program "$@"
|
||||
handle_log_path "$@"
|
||||
init_begin_time
|
||||
|
||||
eval . $dir_shell/otask.sh "$cmd"
|
||||
[[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
|
||||
|
||||
exit 0
|
||||
|
||||
+466
-451
File diff suppressed because it is too large
Load Diff
+38
@@ -0,0 +1,38 @@
|
||||
const baseUrl = window.__ENV__QlBaseUrl || '/';
|
||||
import { setLocale } from '@umijs/max';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export function rootContainer(container: any) {
|
||||
const locales = {
|
||||
'en': require('./locales/en-US.json'),
|
||||
'zh': require('./locales/zh-CN.json'),
|
||||
};
|
||||
let currentLocale = intl.determineLocale({
|
||||
urlLocaleKey: 'lang',
|
||||
cookieLocaleKey: 'lang',
|
||||
localStorageLocaleKey: 'lang',
|
||||
}).slice(0, 2);
|
||||
|
||||
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
|
||||
currentLocale = 'zh';
|
||||
}
|
||||
|
||||
intl.init({ currentLocale, locales });
|
||||
setLocale(currentLocale === 'zh' ? 'zh-CN' : 'en-US');
|
||||
return container;
|
||||
}
|
||||
|
||||
export function modifyClientRenderOpts(memo: any) {
|
||||
return {
|
||||
...memo,
|
||||
publicPath: baseUrl,
|
||||
basename: baseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyContextOpts(memo: any) {
|
||||
return {
|
||||
...memo,
|
||||
basename: baseUrl,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
|
||||
const { Link } = Typography;
|
||||
|
||||
const Copy = ({ text }: { text: string }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyIdRef = useRef<number>();
|
||||
|
||||
const copyText = (e?: React.MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
|
||||
setCopied(true);
|
||||
|
||||
cleanCopyId();
|
||||
copyIdRef.current = window.setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const cleanCopyId = () => {
|
||||
window.clearTimeout(copyIdRef.current!);
|
||||
};
|
||||
|
||||
return (
|
||||
<Link onClick={copyText} style={{ marginLeft: 1 }}>
|
||||
<CopyToClipboard text={text}>
|
||||
<Tooltip
|
||||
key="copy"
|
||||
title={copied ? intl.get('复制成功') : intl.get('复制')}
|
||||
>
|
||||
{copied ? <CheckOutlined /> : <CopyOutlined />}
|
||||
</Tooltip>
|
||||
</CopyToClipboard>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default Copy;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user