mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 11:22:58 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -10,3 +10,4 @@ SECRET='whyour'
|
|||||||
QINIU_AK=''
|
QINIU_AK=''
|
||||||
QINIU_SK=''
|
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:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- 'develop'
|
- 'develop'
|
||||||
# Sequence of patterns matched against refs/tags
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
schedule:
|
schedule:
|
||||||
# 参考 https://jasonet.co/posts/scheduled-actions/
|
# 参考 https://jasonet.co/posts/scheduled-actions/
|
||||||
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
|
- cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
|
||||||
# minute hour day month dayOfWeek
|
|
||||||
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
|
|
||||||
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
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:
|
build-static:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: build front and back
|
- name: build front and back
|
||||||
run: |
|
run: |
|
||||||
npm i -g pnpm
|
pnpm install --frozen-lockfile
|
||||||
pnpm install
|
|
||||||
pnpm build:front
|
pnpm build:front
|
||||||
pnpm build:back
|
pnpm build:back
|
||||||
|
|
||||||
@@ -34,6 +61,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
||||||
GITHUB_BRANCH: ${{ github.ref_name }}
|
GITHUB_BRANCH: ${{ github.ref_name }}
|
||||||
|
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: |
|
run: |
|
||||||
mkdir -p tmp
|
mkdir -p tmp
|
||||||
cd ./tmp
|
cd ./tmp
|
||||||
@@ -44,7 +74,19 @@ jobs:
|
|||||||
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
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}
|
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:
|
build:
|
||||||
|
if: github.ref != 'refs/heads/master'
|
||||||
|
|
||||||
needs: build-static
|
needs: build-static
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -55,14 +97,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: pnpm/action-setup@v2
|
||||||
|
|
||||||
- name: Set time zone
|
|
||||||
uses: szenius/set-timezone@v1.0
|
|
||||||
with:
|
with:
|
||||||
timezoneLinux: "Asia/Shanghai"
|
version: latest
|
||||||
timezoneMacos: "Asia/Shanghai"
|
- uses: actions/setup-node@v3
|
||||||
timezoneWindows: "China Standard Time"
|
with:
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Setup timezone
|
||||||
|
uses: zcong1993/setup-timezone@master
|
||||||
|
with:
|
||||||
|
timezone: Asia/Shanghai
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
@@ -108,9 +153,16 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
MAINTAINER=${{ github.repository_owner }}
|
MAINTAINER=${{ github.repository_owner }}
|
||||||
QL_BRANCH=${{ github.ref_name }}
|
QL_BRANCH=${{ github.ref_name }}
|
||||||
|
SOURCE_COMMIT=${{ github.sha }}
|
||||||
network: host
|
network: host
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||||
context: docker/
|
context: .
|
||||||
|
file: ./docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
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 }}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
/npm-debug.log*
|
/npm-debug.log*
|
||||||
/yarn-error.log
|
/yarn-error.log
|
||||||
/yarn.lock
|
/yarn.lock
|
||||||
/pnpm-lock.yaml
|
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
|
||||||
# production
|
# production
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import { defineConfig } from 'umi';
|
import { defineConfig } from '@umijs/max';
|
||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
hash: true,
|
hash: true,
|
||||||
layout: false,
|
antd: {},
|
||||||
outputPath: 'static/dist',
|
outputPath: 'static/dist',
|
||||||
nodeModulesTransform: {
|
fastRefresh: true,
|
||||||
type: 'none',
|
favicons: ['./images/favicon.svg'],
|
||||||
|
mfsu: {
|
||||||
|
strategy: 'eager',
|
||||||
},
|
},
|
||||||
fastRefresh: {},
|
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||||
esbuild: {},
|
|
||||||
webpack5: {},
|
|
||||||
dynamicImport: {
|
|
||||||
loading: '@/components/pageLoading',
|
|
||||||
},
|
|
||||||
favicon: '/images/favicon.svg',
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/public': {
|
'/api/public': {
|
||||||
target: 'http://127.0.0.1:5400/',
|
target: 'http://127.0.0.1:5400/',
|
||||||
@@ -26,7 +22,7 @@ export default defineConfig({
|
|||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
chainWebpack: (config) => {
|
chainWebpack: ((config: any) => {
|
||||||
config.plugin('compression-webpack-plugin').use(
|
config.plugin('compression-webpack-plugin').use(
|
||||||
new CompressionPlugin({
|
new CompressionPlugin({
|
||||||
algorithm: 'gzip',
|
algorithm: 'gzip',
|
||||||
@@ -35,13 +31,13 @@ export default defineConfig({
|
|||||||
minRatio: 0.6,
|
minRatio: 0.6,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
}) as any,
|
||||||
externals: {
|
externals: {
|
||||||
react: 'window.React',
|
react: 'window.React',
|
||||||
'react-dom': 'window.ReactDOM',
|
'react-dom': 'window.ReactDOM',
|
||||||
},
|
},
|
||||||
scripts: [
|
headScripts: [
|
||||||
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
|
||||||
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
|
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
+9
-5
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/whyour/qinglong">
|
<a href="https://github.com/whyour/qinglong">
|
||||||
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ Timed task management panel with python3, javaScript, shell, typescript support
|
|||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[](https://whyour.cn)
|
[](https://whyour.cn)
|
||||||
|
|
||||||
[简体中文](./README.md) | English
|
[简体中文](./README.md) | English
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ Timed task management panel with python3, javaScript, shell, typescript support
|
|||||||
### Local Deployment
|
### Local Deployment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# To be completed
|
# To be refined, see the development steps first (not supported on windows yet)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Podman Deployment
|
### Podman Deployment
|
||||||
@@ -61,7 +61,6 @@ podman run -dit \
|
|||||||
-p 5700:5700 \
|
-p 5700:5700 \
|
||||||
--name qinglong \
|
--name qinglong \
|
||||||
--hostname qinglong \
|
--hostname qinglong \
|
||||||
--restart unless-stopped \
|
|
||||||
docker.io/whyour/qinglong:latest
|
docker.io/whyour/qinglong:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -162,6 +161,10 @@ task <file_path> now
|
|||||||
task <file_path> conc <env_name> <account_number>(Optional)
|
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
|
# 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
|
2. parameter description
|
||||||
@@ -176,6 +179,7 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
* file_path: File path for task execution
|
* 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
|
* 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
|
* account_number: Specify the account number of an environment variable to be executed when the task is executed
|
||||||
|
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
@@ -183,7 +187,7 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||||
- [Ant Design](https://ant.design)
|
- [Ant Design](https://ant.design)
|
||||||
- [Ant Design Pro](https://pro.ant.design/)
|
- [Ant Design Pro](https://pro.ant.design/)
|
||||||
- [Umijs3.0](https://umijs.org)
|
- [Umijs](https://umijs.org)
|
||||||
- [darkreader](https://github.com/darkreader/darkreader)
|
- [darkreader](https://github.com/darkreader/darkreader)
|
||||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/whyour/qinglong">
|
<a href="https://github.com/whyour/qinglong">
|
||||||
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[](https://whyour.cn)
|
[](https://whyour.cn)
|
||||||
|
|
||||||
简体中文 | [English](./README-en.md)
|
简体中文 | [English](./README-en.md)
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
### 本机部署
|
### 本机部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 待完善
|
# 待完善,可先参考开发步骤 (windows暂时不支持)
|
||||||
```
|
```
|
||||||
|
|
||||||
### podman 部署
|
### podman 部署
|
||||||
@@ -61,7 +61,6 @@ podman run -dit \
|
|||||||
-p 5700:5700 \
|
-p 5700:5700 \
|
||||||
--name qinglong \
|
--name qinglong \
|
||||||
--hostname qinglong \
|
--hostname qinglong \
|
||||||
--restart unless-stopped \
|
|
||||||
docker.io/whyour/qinglong:latest
|
docker.io/whyour/qinglong:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -161,6 +160,10 @@ task <file_path> now
|
|||||||
task <file_path> conc <env_name> <account_number>(可选的)
|
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. 参数说明
|
2. 参数说明
|
||||||
@@ -176,6 +179,7 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
* file_path: 任务执行时的文件路径
|
* file_path: 任务执行时的文件路径
|
||||||
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||||
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||||
|
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
@@ -183,7 +187,7 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||||
- [Ant Design](https://ant.design)
|
- [Ant Design](https://ant.design)
|
||||||
- [Ant Design Pro](https://pro.ant.design/)
|
- [Ant Design Pro](https://pro.ant.design/)
|
||||||
- [Umijs3.0](https://umijs.org)
|
- [Umijs](https://umijs.org)
|
||||||
- [darkreader](https://github.com/darkreader/darkreader)
|
- [darkreader](https://github.com/darkreader/darkreader)
|
||||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -28,8 +28,9 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
sorts: Joi.array().optional(),
|
sorts: Joi.array().optional().allow(null),
|
||||||
filters: Joi.array().optional(),
|
filters: Joi.array().optional(),
|
||||||
|
filterRelation: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -49,15 +50,20 @@ export default (app: Router) => {
|
|||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
sorts: Joi.array().optional(),
|
sorts: Joi.array().optional().allow(null),
|
||||||
filters: Joi.array().optional(),
|
filters: Joi.array().optional(),
|
||||||
|
filterRelation: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const cronViewService = Container.get(CronViewService);
|
const cronViewService = Container.get(CronViewService);
|
||||||
|
if (req.body.type === 1) {
|
||||||
|
return res.send({ code: 400, message: '参数错误' });
|
||||||
|
} else {
|
||||||
const data = await cronViewService.update(req.body);
|
const data = await cronViewService.update(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-2
@@ -3,8 +3,21 @@ import { Container } from 'typedi';
|
|||||||
import EnvService from '../services/env';
|
import EnvService from '../services/env';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import multer from 'multer';
|
||||||
|
import config from '../config';
|
||||||
|
import fs from 'fs';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.scriptPath);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, file.originalname);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/envs', route);
|
app.use('/envs', route);
|
||||||
|
|
||||||
@@ -26,7 +39,9 @@ export default (app: Router) => {
|
|||||||
body: Joi.array().items(
|
body: Joi.array().items(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
value: Joi.string().required(),
|
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(''),
|
remarks: Joi.string().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -94,7 +109,6 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.move(req.params.id, req.body);
|
const data = await envService.move(req.params.id, req.body);
|
||||||
@@ -177,4 +191,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 = JSON.parse(fileContent);
|
||||||
|
const data = Array.isArray(parseContent)
|
||||||
|
? parseContent
|
||||||
|
: [parseContent];
|
||||||
|
if (data.every((x) => x.name && x.value)) {
|
||||||
|
const result = await envService.create(
|
||||||
|
data.map((x) => ({
|
||||||
|
name: x.name,
|
||||||
|
value: x.value,
|
||||||
|
remarks: x.remarks,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return res.send({ code: 200, data: result });
|
||||||
|
} else {
|
||||||
|
return res.send({
|
||||||
|
code: 400,
|
||||||
|
message: '文件缺少name或者value字段,参考导出文件格式',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+32
-1
@@ -3,8 +3,9 @@ import { Container } from 'typedi';
|
|||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { getFileContentByName, readDirs } from '../config/util';
|
import { emptyDir, getFileContentByName, readDirs } from '../config/util';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
const blacklist = ['.tmp'];
|
const blacklist = ['.tmp'];
|
||||||
|
|
||||||
@@ -45,4 +46,34 @@ 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
+51
-7
@@ -4,6 +4,7 @@ import {
|
|||||||
readDirs,
|
readDirs,
|
||||||
getLastModifyFilePath,
|
getLastModifyFilePath,
|
||||||
readDir,
|
readDir,
|
||||||
|
emptyDir,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
@@ -77,11 +78,13 @@ export default (app: Router) => {
|
|||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path, content, originFilename } = req.body as {
|
let { filename, path, content, originFilename, directory } =
|
||||||
|
req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
path: string;
|
||||||
content: string;
|
content: string;
|
||||||
originFilename: string;
|
originFilename: string;
|
||||||
|
directory: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
@@ -105,11 +108,19 @@ export default (app: Router) => {
|
|||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (directory) {
|
||||||
|
fs.mkdirSync(join(path, directory), { recursive: true });
|
||||||
|
return res.send({ code: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
if (!originFilename) {
|
if (!originFilename) {
|
||||||
originFilename = filename;
|
originFilename = filename;
|
||||||
}
|
}
|
||||||
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
|
const originFilePath = join(
|
||||||
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
path,
|
||||||
|
`${originFilename.replace(/\//g, '')}`,
|
||||||
|
);
|
||||||
|
const filePath = join(path, `${filename.replace(/\//g, '')}`);
|
||||||
if (fs.existsSync(originFilePath)) {
|
if (fs.existsSync(originFilePath)) {
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
originFilePath,
|
originFilePath,
|
||||||
@@ -159,17 +170,23 @@ export default (app: Router) => {
|
|||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().allow(''),
|
path: Joi.string().allow(''),
|
||||||
|
type: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path } = req.body as {
|
let { filename, path, type } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
type: string;
|
||||||
};
|
};
|
||||||
const filePath = join(config.scriptPath, path, filename);
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
|
if (type === 'directory') {
|
||||||
|
emptyDir(filePath);
|
||||||
|
} else {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -238,23 +255,50 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
content: Joi.string().optional().allow(''),
|
|
||||||
path: Joi.string().optional().allow(''),
|
path: Joi.string().optional().allow(''),
|
||||||
|
pid: Joi.number().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, content, path } = req.body;
|
let { filename, path, pid } = req.body;
|
||||||
const { name, ext } = parse(filename);
|
const { name, ext } = parse(filename);
|
||||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||||
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.stopScript(filePath);
|
const result = await scriptService.stopScript(filePath, pid);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(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),
|
sub_after: Joi.string().optional().allow('').allow(null),
|
||||||
schedule_type: Joi.string().required(),
|
schedule_type: Joi.string().required(),
|
||||||
alias: 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) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -177,6 +180,9 @@ export default (app: Router) => {
|
|||||||
sub_before: Joi.string().optional().allow('').allow(null),
|
sub_before: Joi.string().optional().allow('').allow(null),
|
||||||
sub_after: Joi.string().optional().allow('').allow(null),
|
sub_after: Joi.string().optional().allow('').allow(null),
|
||||||
alias: 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),
|
||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
+24
-4
@@ -7,6 +7,9 @@ import SystemService from '../services/system';
|
|||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
import { EnvModel } from '../data/env';
|
import { EnvModel } from '../data/env';
|
||||||
|
import { parseVersion, promiseExec } from '../config/util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
@@ -18,10 +21,22 @@ export default (app: Router) => {
|
|||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const authInfo = await userService.getUserInfo();
|
const authInfo = await userService.getUserInfo();
|
||||||
const envCount = await EnvModel.count();
|
const envCount = await EnvModel.count();
|
||||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
const { version, changeLog, changeLogLink } = await parseVersion(
|
||||||
|
config.versionFile,
|
||||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
);
|
||||||
const version = currentVersionFile.match(versionRegx)![1];
|
const lastCommitTime = (
|
||||||
|
await promiseExec(
|
||||||
|
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
|
||||||
|
)
|
||||||
|
).replace('\n', '');
|
||||||
|
const lastCommitId = (
|
||||||
|
await promiseExec(`cd ${config.rootPath} && git rev-parse --short HEAD`)
|
||||||
|
).replace('\n', '');
|
||||||
|
const branch = (
|
||||||
|
await promiseExec(
|
||||||
|
`cd ${config.rootPath} && git symbolic-ref --short HEAD`,
|
||||||
|
)
|
||||||
|
).replace('\n', '');
|
||||||
|
|
||||||
let isInitialized = true;
|
let isInitialized = true;
|
||||||
if (
|
if (
|
||||||
@@ -37,6 +52,11 @@ export default (app: Router) => {
|
|||||||
data: {
|
data: {
|
||||||
isInitialized,
|
isInitialized,
|
||||||
version,
|
version,
|
||||||
|
lastCommitTime: dayjs(lastCommitTime).unix(),
|
||||||
|
lastCommitId,
|
||||||
|
branch,
|
||||||
|
changeLog,
|
||||||
|
changeLogLink,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ async function startServer() {
|
|||||||
|
|
||||||
const server = app
|
const server = app
|
||||||
.listen(config.port, () => {
|
.listen(config.port, () => {
|
||||||
Logger.debug(`✌️ Back server launched on port ${config.port}`);
|
Logger.debug(`✌️ 后端服务启动成功!`);
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
|||||||
@@ -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} `;
|
||||||
@@ -11,10 +11,10 @@ if (!process.env.QL_DIR) {
|
|||||||
if (qlHomePath.endsWith('/static/')) {
|
if (qlHomePath.endsWith('/static/')) {
|
||||||
qlHomePath = path.join(qlHomePath, '../');
|
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 rootPath = process.env.QL_DIR as string;
|
||||||
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||||
@@ -40,7 +40,7 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
|
|||||||
const authError = '错误的用户名密码,请重试';
|
const authError = '错误的用户名密码,请重试';
|
||||||
const loginFaild = '请先登录!';
|
const loginFaild = '请先登录!';
|
||||||
const configString = 'config sample crontab shareCode diy';
|
const configString = 'config sample crontab shareCode diy';
|
||||||
const versionFile = path.join(rootPath, 'src/version.ts');
|
const versionFile = path.join(rootPath, 'version.yaml');
|
||||||
|
|
||||||
if (envFound.error) {
|
if (envFound.error) {
|
||||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||||
|
|||||||
+150
-8
@@ -3,6 +3,10 @@ import * as path from 'path';
|
|||||||
import got from 'got';
|
import got from 'got';
|
||||||
import iconv from 'iconv-lite';
|
import iconv from 'iconv-lite';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import psTreeFun from 'pstree.remy';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { load } from 'js-yaml';
|
||||||
|
|
||||||
export function getFileContentByName(fileName: string) {
|
export function getFileContentByName(fileName: string) {
|
||||||
if (fs.existsSync(fileName)) {
|
if (fs.existsSync(fileName)) {
|
||||||
@@ -283,6 +287,20 @@ enum FileType {
|
|||||||
'file',
|
'file',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IFile {
|
||||||
|
title: string;
|
||||||
|
key: string;
|
||||||
|
type: 'directory' | 'file';
|
||||||
|
parent: string;
|
||||||
|
mtime: number;
|
||||||
|
children?: IFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dirSort(a: IFile, b: IFile) {
|
||||||
|
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1;
|
||||||
|
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
export function readDirs(
|
export function readDirs(
|
||||||
dir: string,
|
dir: string,
|
||||||
baseDir: string = '',
|
baseDir: string = '',
|
||||||
@@ -301,24 +319,21 @@ export function readDirs(
|
|||||||
title: file,
|
title: file,
|
||||||
key,
|
key,
|
||||||
type: 'directory',
|
type: 'directory',
|
||||||
disabled: true,
|
|
||||||
parent: relativePath,
|
parent: relativePath,
|
||||||
children: readDirs(subPath, baseDir).sort(
|
mtime: stats.mtime.getTime(),
|
||||||
(a: any, b: any) =>
|
children: readDirs(subPath, baseDir).sort(dirSort),
|
||||||
(FileType as any)[a.type] - (FileType as any)[b.type],
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
title: file,
|
title: file,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
|
isLeaf: true,
|
||||||
key,
|
key,
|
||||||
parent: relativePath,
|
parent: relativePath,
|
||||||
|
mtime: stats.mtime.getTime(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return result.sort(
|
return result.sort(dirSort);
|
||||||
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDir(
|
export function readDir(
|
||||||
@@ -344,6 +359,20 @@ export function readDir(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function emptyDir(path: string) {
|
||||||
|
const files = fs.readdirSync(path);
|
||||||
|
files.forEach((file) => {
|
||||||
|
const filePath = `${path}/${file}`;
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
emptyDir(filePath);
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fs.rmdirSync(path);
|
||||||
|
}
|
||||||
|
|
||||||
export function promiseExec(command: string): Promise<string> {
|
export function promiseExec(command: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(
|
exec(
|
||||||
@@ -355,3 +384,116 @@ export function promiseExec(command: string): Promise<string> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseHeaders(headers: string) {
|
||||||
|
if (!headers) return {};
|
||||||
|
|
||||||
|
const parsed: any = {};
|
||||||
|
let key;
|
||||||
|
let val;
|
||||||
|
let i;
|
||||||
|
|
||||||
|
headers &&
|
||||||
|
headers.split('\n').forEach(function parser(line) {
|
||||||
|
i = line.indexOf(':');
|
||||||
|
key = line.substring(0, i).trim().toLowerCase();
|
||||||
|
val = line.substring(i + 1).trim();
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBody(
|
||||||
|
body: string,
|
||||||
|
contentType:
|
||||||
|
| 'application/json'
|
||||||
|
| 'multipart/form-data'
|
||||||
|
| 'application/x-www-form-urlencoded',
|
||||||
|
) {
|
||||||
|
if (!body) return '';
|
||||||
|
|
||||||
|
const parsed: any = {};
|
||||||
|
let key;
|
||||||
|
let val;
|
||||||
|
let i;
|
||||||
|
|
||||||
|
body &&
|
||||||
|
body.split('\n').forEach(function parser(line) {
|
||||||
|
i = line.indexOf(':');
|
||||||
|
key = line.substring(0, i).trim().toLowerCase();
|
||||||
|
val = line.substring(i + 1).trim();
|
||||||
|
|
||||||
|
if (!key || parsed[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonValue = JSON.parse(val);
|
||||||
|
parsed[key] = jsonValue;
|
||||||
|
} catch (error) {
|
||||||
|
parsed[key] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (contentType) {
|
||||||
|
case 'multipart/form-data':
|
||||||
|
return Object.keys(parsed).reduce((p, c) => {
|
||||||
|
p.append(c, parsed[c]);
|
||||||
|
return p;
|
||||||
|
}, new FormData());
|
||||||
|
case 'application/x-www-form-urlencoded':
|
||||||
|
return Object.keys(parsed).reduce((p, c) => {
|
||||||
|
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function psTree(pid: number): Promise<number[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
psTreeFun(pid, (err: any, pids: number[]) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(pids.filter((x) => !isNaN(x)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function killTask(pid: number) {
|
||||||
|
const pids = await psTree(pid);
|
||||||
|
// SIGALRM 14 时钟信号
|
||||||
|
if (pids.length) {
|
||||||
|
process.kill(pids[0], 14);
|
||||||
|
} else {
|
||||||
|
process.kill(pid, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPid(name: string) {
|
||||||
|
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
let pid = (await execAsync(taskCommand)).stdout;
|
||||||
|
return Number(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IVersion {
|
||||||
|
version: string;
|
||||||
|
changeLogLink: string;
|
||||||
|
changeLog: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseVersion(path: string): Promise<IVersion> {
|
||||||
|
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseContentVersion(content: string): Promise<IVersion> {
|
||||||
|
return load(content) as IVersion;
|
||||||
|
}
|
||||||
|
|||||||
+16
-1
@@ -1,13 +1,19 @@
|
|||||||
import { sequelize } from '.';
|
import { sequelize } from '.';
|
||||||
import { DataTypes, Model } from 'sequelize';
|
import { DataTypes, Model } from 'sequelize';
|
||||||
|
|
||||||
|
export enum CronViewType {
|
||||||
|
'系统' = 1,
|
||||||
|
'个人',
|
||||||
|
}
|
||||||
|
|
||||||
interface SortType {
|
interface SortType {
|
||||||
type: 'ASC' | 'DESC';
|
type: 'ASC' | 'DESC';
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterType {
|
interface FilterType {
|
||||||
type: 'or' | 'and';
|
property: string;
|
||||||
|
operation: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +24,8 @@ export class CrontabView {
|
|||||||
isDisabled?: 1 | 0;
|
isDisabled?: 1 | 0;
|
||||||
filters?: FilterType[];
|
filters?: FilterType[];
|
||||||
sorts?: SortType[];
|
sorts?: SortType[];
|
||||||
|
filterRelation?: 'and' | 'or';
|
||||||
|
type?: CronViewType;
|
||||||
|
|
||||||
constructor(options: CrontabView) {
|
constructor(options: CrontabView) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
@@ -26,6 +34,8 @@ export class CrontabView {
|
|||||||
this.isDisabled = options.isDisabled || 0;
|
this.isDisabled = options.isDisabled || 0;
|
||||||
this.filters = options.filters;
|
this.filters = options.filters;
|
||||||
this.sorts = options.sorts;
|
this.sorts = options.sorts;
|
||||||
|
this.filterRelation = options.filterRelation;
|
||||||
|
this.type = options.type || CronViewType.个人;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,5 +53,10 @@ export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
|||||||
isDisabled: DataTypes.NUMBER,
|
isDisabled: DataTypes.NUMBER,
|
||||||
filters: DataTypes.JSON,
|
filters: DataTypes.JSON,
|
||||||
sorts: DataTypes.JSON,
|
sorts: DataTypes.JSON,
|
||||||
|
filterRelation: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+4
-1
@@ -26,7 +26,10 @@ export enum EnvStatus {
|
|||||||
'disabled',
|
'disabled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initEnvPosition = 9999999999;
|
export const maxPosition = 9000000000000000;
|
||||||
|
export const initPosition = 4500000000000000;
|
||||||
|
export const stepPosition = 10000000;
|
||||||
|
export const minPosition = 100;
|
||||||
|
|
||||||
interface EnvInstance extends Model<Env, Env>, Env {}
|
interface EnvInstance extends Model<Env, Env>, Env {}
|
||||||
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||||
|
|||||||
+12
-5
@@ -1,15 +1,22 @@
|
|||||||
import { Sequelize } from 'sequelize';
|
import { Sequelize, Transaction } from 'sequelize';
|
||||||
import config from '../config/index';
|
import config from '../config/index';
|
||||||
|
|
||||||
export const sequelize = new Sequelize({
|
export const sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: `${config.dbPath}database.sqlite`,
|
storage: `${config.dbPath}database.sqlite`,
|
||||||
logging: false,
|
logging: false,
|
||||||
pool: {
|
retry: {
|
||||||
max: 6,
|
max: 10,
|
||||||
min: 0,
|
match: ['SQLITE_BUSY: database is locked'],
|
||||||
idle: 30000,
|
|
||||||
},
|
},
|
||||||
|
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 };
|
export type ResponseType<T> = { code: number; data?: T; message?: string };
|
||||||
|
|||||||
+39
-2
@@ -1,16 +1,22 @@
|
|||||||
|
import { IncomingHttpHeaders } from 'http';
|
||||||
|
|
||||||
export enum NotificationMode {
|
export enum NotificationMode {
|
||||||
'gotify' = 'gotify',
|
'gotify' = 'gotify',
|
||||||
'goCqHttpBot' = 'goCqHttpBot',
|
'goCqHttpBot' = 'goCqHttpBot',
|
||||||
'serverChan' = 'serverChan',
|
'serverChan' = 'serverChan',
|
||||||
'pushDeer' = 'pushDeer',
|
'pushDeer' = 'pushDeer',
|
||||||
'bark' = 'bark',
|
'bark' = 'bark',
|
||||||
|
'chat' = 'chat',
|
||||||
'telegramBot' = 'telegramBot',
|
'telegramBot' = 'telegramBot',
|
||||||
'dingtalkBot' = 'dingtalkBot',
|
'dingtalkBot' = 'dingtalkBot',
|
||||||
'weWorkBot' = 'weWorkBot',
|
'weWorkBot' = 'weWorkBot',
|
||||||
'weWorkApp' = 'weWorkApp',
|
'weWorkApp' = 'weWorkApp',
|
||||||
|
'aibotk' = 'aibotk',
|
||||||
'iGot' = 'iGot',
|
'iGot' = 'iGot',
|
||||||
'pushPlus' = 'pushPlus',
|
'pushPlus' = 'pushPlus',
|
||||||
'email' = 'email',
|
'email' = 'email',
|
||||||
|
'feishu' = 'feishu',
|
||||||
|
'webhook' = 'webhook',
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class NotificationBaseInfo {
|
abstract class NotificationBaseInfo {
|
||||||
@@ -35,11 +41,17 @@ export class ServerChanNotification extends NotificationBaseInfo {
|
|||||||
|
|
||||||
export class PushDeerNotification extends NotificationBaseInfo {
|
export class PushDeerNotification extends NotificationBaseInfo {
|
||||||
public pushDeerKey = '';
|
public pushDeerKey = '';
|
||||||
|
public pushDeerUrl = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChatNotification extends NotificationBaseInfo {
|
||||||
|
public chatUrl = '';
|
||||||
|
public chatToken = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BarkNotification extends NotificationBaseInfo {
|
export class BarkNotification extends NotificationBaseInfo {
|
||||||
public barkPush = '';
|
public barkPush = '';
|
||||||
public barkIcon = 'http://qn.whyour.cn/logo.png';
|
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
||||||
public barkSound = '';
|
public barkSound = '';
|
||||||
public barkGroup = 'qinglong';
|
public barkGroup = 'qinglong';
|
||||||
}
|
}
|
||||||
@@ -66,6 +78,12 @@ export class WeWorkAppNotification extends NotificationBaseInfo {
|
|||||||
public weWorkAppKey = '';
|
public weWorkAppKey = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AibotkNotification extends NotificationBaseInfo {
|
||||||
|
public aibotkKey: string = '';
|
||||||
|
public aibotkType: 'room' | 'contact' = 'room';
|
||||||
|
public aibotkName: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
export class IGotNotification extends NotificationBaseInfo {
|
export class IGotNotification extends NotificationBaseInfo {
|
||||||
public iGotPushKey = '';
|
public iGotPushKey = '';
|
||||||
}
|
}
|
||||||
@@ -81,16 +99,35 @@ export class EmailNotification extends NotificationBaseInfo {
|
|||||||
public emailPass: string = '';
|
public emailPass: string = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class WebhookNotification extends NotificationBaseInfo {
|
||||||
|
public webhookHeaders: string = '';
|
||||||
|
public webhookBody: string = '';
|
||||||
|
public webhookUrl: string = '';
|
||||||
|
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
|
||||||
|
public webhookContentType:
|
||||||
|
| 'application/json'
|
||||||
|
| 'multipart/form-data'
|
||||||
|
| 'application/x-www-form-urlencoded' = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LarkNotification extends NotificationBaseInfo {
|
||||||
|
public larkKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotificationInfo
|
export interface NotificationInfo
|
||||||
extends GoCqHttpBotNotification,
|
extends GoCqHttpBotNotification,
|
||||||
GotifyNotification,
|
GotifyNotification,
|
||||||
ServerChanNotification,
|
ServerChanNotification,
|
||||||
PushDeerNotification,
|
PushDeerNotification,
|
||||||
|
ChatNotification,
|
||||||
BarkNotification,
|
BarkNotification,
|
||||||
TelegramBotNotification,
|
TelegramBotNotification,
|
||||||
DingtalkBotNotification,
|
DingtalkBotNotification,
|
||||||
WeWorkBotNotification,
|
WeWorkBotNotification,
|
||||||
WeWorkAppNotification,
|
WeWorkAppNotification,
|
||||||
|
AibotkNotification,
|
||||||
IGotNotification,
|
IGotNotification,
|
||||||
PushPlusNotification,
|
PushPlusNotification,
|
||||||
EmailNotification {}
|
EmailNotification,
|
||||||
|
WebhookNotification,
|
||||||
|
LarkNotification {}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export class Subscription {
|
|||||||
extensions?: string;
|
extensions?: string;
|
||||||
sub_before?: string;
|
sub_before?: string;
|
||||||
sub_after?: string;
|
sub_after?: string;
|
||||||
|
proxy?: string;
|
||||||
|
autoAddCron?: 1 | 0;
|
||||||
|
autoDelCron?: 1 | 0;
|
||||||
|
|
||||||
constructor(options: Subscription) {
|
constructor(options: Subscription) {
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
@@ -54,6 +57,9 @@ export class Subscription {
|
|||||||
this.extensions = options.extensions;
|
this.extensions = options.extensions;
|
||||||
this.sub_before = options.sub_before;
|
this.sub_before = options.sub_before;
|
||||||
this.sub_after = options.sub_after;
|
this.sub_after = options.sub_after;
|
||||||
|
this.proxy = options.proxy;
|
||||||
|
this.autoAddCron = options.autoAddCron ? 1 : 0;
|
||||||
|
this.autoDelCron = options.autoDelCron ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,5 +108,8 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
|||||||
log_path: DataTypes.STRING,
|
log_path: DataTypes.STRING,
|
||||||
schedule_type: DataTypes.STRING,
|
schedule_type: DataTypes.STRING,
|
||||||
alias: { type: DataTypes.STRING, unique: 'alias' },
|
alias: { type: DataTypes.STRING, unique: 'alias' },
|
||||||
|
proxy: { type: DataTypes.STRING, allowNull: true },
|
||||||
|
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
|
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+25
-6
@@ -10,6 +10,7 @@ import { fileExist } from '../config/util';
|
|||||||
import { SubscriptionModel } from '../data/subscription';
|
import { SubscriptionModel } from '../data/subscription';
|
||||||
import { CrontabViewModel } from '../data/cronView';
|
import { CrontabViewModel } from '../data/cronView';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { sequelize } from '../data';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
try {
|
try {
|
||||||
@@ -21,12 +22,30 @@ export default async () => {
|
|||||||
await SubscriptionModel.sync();
|
await SubscriptionModel.sync();
|
||||||
await CrontabViewModel.sync();
|
await CrontabViewModel.sync();
|
||||||
|
|
||||||
// try {
|
// 初始化新增字段
|
||||||
// const queryInterface = sequelize.getQueryInterface();
|
try {
|
||||||
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
|
await sequelize.query(
|
||||||
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
|
'alter table CrontabViews add column filterRelation VARCHAR(255)',
|
||||||
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
|
);
|
||||||
// } catch (error) { }
|
} 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) {}
|
||||||
|
|
||||||
// 2.10-2.11 升级
|
// 2.10-2.11 升级
|
||||||
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
||||||
|
|||||||
+19
-7
@@ -3,7 +3,7 @@ import bodyParser from 'body-parser';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import routes from '../api';
|
import routes from '../api';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import jwt from 'express-jwt';
|
import jwt, { UnauthorizedError } from 'express-jwt';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { getPlatform, getToken } from '../config/util';
|
import { getPlatform, getToken } from '../config/util';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
@@ -75,22 +75,24 @@ export default ({ app }: { app: Application }) => {
|
|||||||
if (
|
if (
|
||||||
!headerToken &&
|
!headerToken &&
|
||||||
originPath &&
|
originPath &&
|
||||||
config.apiWhiteList.includes(originPath) &&
|
config.apiWhiteList.includes(originPath)
|
||||||
originPath !== '/api/crons/status'
|
|
||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
if (data) {
|
if (data && headerToken) {
|
||||||
const { token = '', tokens = {} } = JSON.parse(data);
|
const { token = '', tokens = {} } = JSON.parse(data);
|
||||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const err: any = new Error('UnauthorizedError');
|
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
|
||||||
err.status = 401;
|
const errorMessage = headerToken
|
||||||
|
? 'jwt malformed'
|
||||||
|
: 'No authorization token was found';
|
||||||
|
const err = new UnauthorizedError(errorCode, { message: errorMessage });
|
||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,7 +170,17 @@ export default ({ app }: { app: Application }) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(Sentry.Handlers.errorHandler());
|
app.use(
|
||||||
|
Sentry.Handlers.errorHandler({
|
||||||
|
shouldHandleError(error) {
|
||||||
|
// 排除 SequelizeUniqueConstraintError / NotFound
|
||||||
|
return (
|
||||||
|
!['SequelizeUniqueConstraintError'].includes(error.name) ||
|
||||||
|
!['Not Found'].includes(error.message)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -4,16 +4,32 @@ import { Container } from 'typedi';
|
|||||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
import CronService from '../services/cron';
|
import CronService from '../services/cron';
|
||||||
import EnvService from '../services/env';
|
import EnvService from '../services/env';
|
||||||
import _ from 'lodash';
|
import groupBy from 'lodash/groupBy';
|
||||||
import { DependenceModel } from '../data/dependence';
|
import { DependenceModel } from '../data/dependence';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { CrontabViewModel, CronViewType } from '../data/cronView';
|
||||||
|
import { initPosition } from '../data/env';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const dependenceService = Container.get(DependenceService);
|
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(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.idle },
|
{ status: CrontabStatus.idle },
|
||||||
@@ -26,7 +42,7 @@ export default async () => {
|
|||||||
order: [['type', 'DESC']],
|
order: [['type', 'DESC']],
|
||||||
raw: true,
|
raw: true,
|
||||||
}).then(async (docs) => {
|
}).then(async (docs) => {
|
||||||
const groups = _.groupBy(docs, 'type');
|
const groups = groupBy(docs, 'type');
|
||||||
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
|
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const group = groups[key];
|
const group = groups[key];
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import _ from 'lodash';
|
|
||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
import ScheduleService from '../services/schedule';
|
import ScheduleService from '../services/schedule';
|
||||||
import SubscriptionService from '../services/subscription';
|
import SubscriptionService from '../services/subscription';
|
||||||
@@ -13,18 +12,17 @@ export default async () => {
|
|||||||
|
|
||||||
// 生成内置token
|
// 生成内置token
|
||||||
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
|
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
|
||||||
const tokenFile = `${config.rootPath}/static/build/token.js`;
|
const tokenFile = `${config.rootPath}static/build/token.js`;
|
||||||
if (await fileExist(tokenFile)) {
|
if (await fileExist(tokenFile)) {
|
||||||
tokenCommand = `node ${tokenFile}`;
|
tokenCommand = `node ${tokenFile}`;
|
||||||
}
|
}
|
||||||
const cron = {
|
const cron = {
|
||||||
id: 'token',
|
id: NaN,
|
||||||
name: '生成token',
|
name: '生成token',
|
||||||
command: tokenCommand,
|
command: tokenCommand,
|
||||||
};
|
};
|
||||||
scheduleService.createIntervalTask(cron as any, {
|
scheduleService.createIntervalTask(cron, {
|
||||||
days: 28,
|
days: 28,
|
||||||
runImmediately: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 运行删除日志任务
|
// 运行删除日志任务
|
||||||
@@ -37,7 +35,6 @@ export default async () => {
|
|||||||
};
|
};
|
||||||
scheduleService.createIntervalTask(cron, {
|
scheduleService.createIntervalTask(cron, {
|
||||||
days: data.info.frequency,
|
days: data.info.frequency,
|
||||||
runImmediately: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,21 @@ import { Application } from 'express';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import * as Tracing from '@sentry/tracing';
|
import * as Tracing from '@sentry/tracing';
|
||||||
import Logger from './logger';
|
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({
|
Sentry.init({
|
||||||
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
||||||
integrations: [
|
integrations: [
|
||||||
new Sentry.Integrations.Http({ tracing: true }),
|
new Sentry.Integrations.Http({ tracing: true }),
|
||||||
new Tracing.Integrations.Express({ app: expressApp }),
|
new Tracing.Integrations.Express({ app: expressApp }),
|
||||||
],
|
],
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
tracesSampleRate: 1.0,
|
release: version,
|
||||||
});
|
});
|
||||||
|
|
||||||
expressApp.use(Sentry.Handlers.requestHandler());
|
expressApp.use(Sentry.Handlers.requestHandler());
|
||||||
|
|||||||
+3
-6
@@ -6,15 +6,12 @@ import config from './config';
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.get('/api/public/panel/log', (req, res) => {
|
app.get('/api/public/panel/log', (req, res) => {
|
||||||
exec(
|
exec('tail -n 300 ~/.pm2/logs/panel-error.log', (err, stdout, stderr) => {
|
||||||
'pm2 logs panel --lines 500 --nostream --timestamp',
|
|
||||||
(err, stdout, stderr) => {
|
|
||||||
if (err || stderr) {
|
if (err || stderr) {
|
||||||
return res.send({ code: 400, message: (err && err.message) || stderr });
|
return res.send({ code: 400, message: (err && err.message) || stderr });
|
||||||
}
|
}
|
||||||
return res.send({ code: 200, data: stdout });
|
return res.send({ code: 200, data: stdout });
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app
|
app
|
||||||
@@ -22,7 +19,7 @@ app
|
|||||||
await require('./loaders/sentry').default({ expressApp: app });
|
await require('./loaders/sentry').default({ expressApp: app });
|
||||||
await require('./loaders/db').default();
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`);
|
Logger.debug(`✌️ 公共服务启动成功!`);
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
|||||||
+10
-9
@@ -4,6 +4,7 @@ import { exec } from 'child_process';
|
|||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import { CrontabModel, CrontabStatus } from './data/cron';
|
import { CrontabModel, CrontabStatus } from './data/cron';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
import { QL_PREFIX, TASK_PREFIX } from './config/const';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -18,14 +19,18 @@ const run = async () => {
|
|||||||
_schedule &&
|
_schedule &&
|
||||||
_schedule.length > 5 &&
|
_schedule.length > 5 &&
|
||||||
task.status !== CrontabStatus.disabled &&
|
task.status !== CrontabStatus.disabled &&
|
||||||
!task.isDisabled
|
!task.isDisabled &&
|
||||||
|
task.schedule
|
||||||
) {
|
) {
|
||||||
schedule.scheduleJob(task.schedule, function () {
|
schedule.scheduleJob(task.schedule, function () {
|
||||||
let command = task.command as string;
|
let command = task.command as string;
|
||||||
if (!command.includes('task ') && !command.includes('ql ')) {
|
if (
|
||||||
command = `task ${command}`;
|
!command.startsWith(TASK_PREFIX) &&
|
||||||
|
!command.startsWith(QL_PREFIX)
|
||||||
|
) {
|
||||||
|
command = `${TASK_PREFIX}${command}`;
|
||||||
}
|
}
|
||||||
exec(command);
|
exec(`ID=${task.id} ${command}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,11 +48,7 @@ app
|
|||||||
await require('./loaders/db').default();
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
Logger.info(`
|
Logger.debug('定时任务服务启动成功!');
|
||||||
################################################
|
|
||||||
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
|
||||||
################################################
|
|
||||||
`);
|
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
|||||||
+89
-157
@@ -5,16 +5,20 @@ import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
|||||||
import { exec, execSync, spawn } from 'child_process';
|
import { exec, execSync, spawn } from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import cron_parser from 'cron-parser';
|
import cron_parser from 'cron-parser';
|
||||||
import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
|
import {
|
||||||
|
getFileContentByName,
|
||||||
|
concurrentRun,
|
||||||
|
fileExist,
|
||||||
|
killTask,
|
||||||
|
} from '../config/util';
|
||||||
import { promises, existsSync } from 'fs';
|
import { promises, existsSync } from 'fs';
|
||||||
import { promisify } from 'util';
|
import { Op, where, col as colFn } from 'sequelize';
|
||||||
import { Op } from 'sequelize';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import dayjs from 'dayjs';
|
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||||
|
|
||||||
private isSixCron(cron: Crontab) {
|
private isSixCron(cron: Crontab) {
|
||||||
const { schedule } = cron;
|
const { schedule } = cron;
|
||||||
@@ -28,7 +32,7 @@ export default class CronService {
|
|||||||
const tab = new Crontab(payload);
|
const tab = new Crontab(payload);
|
||||||
tab.saved = false;
|
tab.saved = false;
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
await this.set_crontab(this.isSixCron(doc));
|
await this.set_crontab();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +43,7 @@ export default class CronService {
|
|||||||
public async update(payload: Crontab): Promise<Crontab> {
|
public async update(payload: Crontab): Promise<Crontab> {
|
||||||
payload.saved = false;
|
payload.saved = false;
|
||||||
const newDoc = await this.updateDb(payload);
|
const newDoc = await this.updateDb(payload);
|
||||||
await this.set_crontab(this.isSixCron(newDoc));
|
await this.set_crontab();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +82,7 @@ export default class CronService {
|
|||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
await CrontabModel.destroy({ where: { id: ids } });
|
await CrontabModel.destroy({ where: { id: ids } });
|
||||||
await this.set_crontab(true);
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pin(ids: number[]) {
|
public async pin(ids: number[]) {
|
||||||
@@ -115,18 +119,26 @@ export default class CronService {
|
|||||||
|
|
||||||
private formatViewQuery(query: any, viewQuery: any) {
|
private formatViewQuery(query: any, viewQuery: any) {
|
||||||
if (viewQuery.filters && viewQuery.filters.length > 0) {
|
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) {
|
for (const col of viewQuery.filters) {
|
||||||
const { property, value, operation } = col;
|
const { property, value, operation } = col;
|
||||||
|
let q: any = {};
|
||||||
|
let operate2 = null;
|
||||||
let operate = null;
|
let operate = null;
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'Reg':
|
case 'Reg':
|
||||||
operate = Op.like;
|
operate = Op.like;
|
||||||
|
operate2 = Op.or;
|
||||||
break;
|
break;
|
||||||
case 'NotReg':
|
case 'NotReg':
|
||||||
operate = Op.notLike;
|
operate = Op.notLike;
|
||||||
|
operate2 = Op.and;
|
||||||
break;
|
break;
|
||||||
case 'In':
|
case 'In':
|
||||||
query[Op.or] = [
|
q[Op.or] = [
|
||||||
{
|
{
|
||||||
[property]: value,
|
[property]: value,
|
||||||
},
|
},
|
||||||
@@ -136,7 +148,7 @@ export default class CronService {
|
|||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case 'Nin':
|
case 'Nin':
|
||||||
query[Op.and] = [
|
q[Op.and] = [
|
||||||
{
|
{
|
||||||
[property]: {
|
[property]: {
|
||||||
[Op.notIn]: value,
|
[Op.notIn]: value,
|
||||||
@@ -150,20 +162,39 @@ export default class CronService {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (operate) {
|
if (operate && operate2) {
|
||||||
query[property] = {
|
q[property] = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
|
{
|
||||||
|
[operate2]: [
|
||||||
{ [operate]: `%${value}%` },
|
{ [operate]: `%${value}%` },
|
||||||
{ [operate]: `%${encodeURIComponent(value)}%` },
|
{ [operate]: `%${encodeURIComponent(value)}%` },
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[operate2]: [
|
||||||
|
where(colFn(property), operate, `%${value}%`),
|
||||||
|
where(
|
||||||
|
colFn(property),
|
||||||
|
operate,
|
||||||
|
`%${encodeURIComponent(value)}%`,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
query[primaryOperate].push(q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatSearchText(query: any, searchText: string | undefined) {
|
private formatSearchText(query: any, searchText: string | undefined) {
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
|
if (!query[Op.and]) {
|
||||||
|
query[Op.and] = [];
|
||||||
|
}
|
||||||
|
let q: any = {};
|
||||||
const textArray = searchText.split(':');
|
const textArray = searchText.split(':');
|
||||||
switch (textArray[0]) {
|
switch (textArray[0]) {
|
||||||
case 'name':
|
case 'name':
|
||||||
@@ -171,13 +202,11 @@ export default class CronService {
|
|||||||
case 'schedule':
|
case 'schedule':
|
||||||
case 'label':
|
case 'label':
|
||||||
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
|
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
|
||||||
query = {
|
q[column] = {
|
||||||
[column]: {
|
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${textArray[1]}%` },
|
{ [Op.like]: `%${textArray[1]}%` },
|
||||||
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
||||||
],
|
],
|
||||||
},
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -187,8 +216,7 @@ export default class CronService {
|
|||||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
query = {
|
q[Op.or] = [
|
||||||
[Op.or]: [
|
|
||||||
{
|
{
|
||||||
name: reg,
|
name: reg,
|
||||||
},
|
},
|
||||||
@@ -201,10 +229,26 @@ export default class CronService {
|
|||||||
{
|
{
|
||||||
labels: reg,
|
labels: reg,
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
};
|
|
||||||
break;
|
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 (filterKeys[key]) {
|
||||||
|
q[key] = filterKeys[key];
|
||||||
|
}
|
||||||
|
query[Op.and].push(q);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,16 +264,16 @@ export default class CronService {
|
|||||||
searchValue: string;
|
searchValue: string;
|
||||||
page: string;
|
page: string;
|
||||||
size: string;
|
size: string;
|
||||||
sortField: string;
|
sorter: string;
|
||||||
sortType: string;
|
filters: string;
|
||||||
queryString: string;
|
queryString: string;
|
||||||
}): Promise<{ data: Crontab[]; total: number }> {
|
}): Promise<{ data: Crontab[]; total: number }> {
|
||||||
const searchText = params?.searchValue;
|
const searchText = params?.searchValue;
|
||||||
const page = Number(params?.page || '0');
|
const page = Number(params?.page || '0');
|
||||||
const size = Number(params?.size || '0');
|
const size = Number(params?.size || '0');
|
||||||
const sortField = params?.sortField || '';
|
|
||||||
const sortType = params?.sortType || '';
|
|
||||||
const viewQuery = JSON.parse(params?.queryString || '{}');
|
const viewQuery = JSON.parse(params?.queryString || '{}');
|
||||||
|
const filterQuery = JSON.parse(params?.filters || '{}');
|
||||||
|
const sorterQuery = JSON.parse(params?.sorter || '{}');
|
||||||
|
|
||||||
let query: any = {};
|
let query: any = {};
|
||||||
let order = [
|
let order = [
|
||||||
@@ -241,10 +285,14 @@ export default class CronService {
|
|||||||
|
|
||||||
this.formatViewQuery(query, viewQuery);
|
this.formatViewQuery(query, viewQuery);
|
||||||
this.formatSearchText(query, searchText);
|
this.formatSearchText(query, searchText);
|
||||||
|
this.formatFilterQuery(query, filterQuery);
|
||||||
this.formatViewSort(order, viewQuery);
|
this.formatViewSort(order, viewQuery);
|
||||||
|
|
||||||
if (sortType && sortField) {
|
if (sorterQuery) {
|
||||||
order.unshift([sortField, sortType]);
|
const { field, type } = sorterQuery;
|
||||||
|
if (field && type) {
|
||||||
|
order.unshift([field, type]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let condition: any = {
|
let condition: any = {
|
||||||
where: query,
|
where: query,
|
||||||
@@ -284,31 +332,11 @@ export default class CronService {
|
|||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
if (doc.pid) {
|
if (doc.pid) {
|
||||||
try {
|
try {
|
||||||
process.kill(-doc.pid);
|
await killTask(doc.pid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.silly(error);
|
this.logger.silly(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const err = await this.killTask(doc.command);
|
|
||||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
|
||||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
|
||||||
|
|
||||||
const endTime = dayjs();
|
|
||||||
const diffTimeStr = doc.last_execution_time
|
|
||||||
? `,耗时 ${endTime.diff(
|
|
||||||
dayjs(doc.last_execution_time * 1000),
|
|
||||||
'second',
|
|
||||||
)}`
|
|
||||||
: '';
|
|
||||||
if (logFileExist) {
|
|
||||||
const str = err ? `\n${err}` : '';
|
|
||||||
fs.appendFileSync(
|
|
||||||
`${absolutePath}`,
|
|
||||||
`${str}\n## 执行结束... ${endTime.format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
)}${diffTimeStr}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
@@ -317,42 +345,6 @@ 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> {
|
private async runSingle(cronId: number): Promise<number> {
|
||||||
return new Promise(async (resolve: any) => {
|
return new Promise(async (resolve: any) => {
|
||||||
const cron = await this.getDb({ id: cronId });
|
const cron = await this.getDb({ id: cronId });
|
||||||
@@ -370,8 +362,8 @@ export default class CronService {
|
|||||||
this.logger.silly('Original command: ' + command);
|
this.logger.silly('Original command: ' + command);
|
||||||
|
|
||||||
let cmdStr = command;
|
let cmdStr = command;
|
||||||
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
|
||||||
cmdStr = `task ${cmdStr}`;
|
cmdStr = `${TASK_PREFIX}${cmdStr}`;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
cmdStr.endsWith('.js') ||
|
cmdStr.endsWith('.js') ||
|
||||||
@@ -383,7 +375,7 @@ export default class CronService {
|
|||||||
cmdStr = `${cmdStr} now`;
|
cmdStr = `${cmdStr} now`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cp = spawn(cmdStr, { shell: '/bin/bash' });
|
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
|
||||||
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.running, pid: cp.pid },
|
{ status: CrontabStatus.running, pid: cp.pid },
|
||||||
@@ -391,7 +383,7 @@ export default class CronService {
|
|||||||
);
|
);
|
||||||
cp.stderr.on('data', (data) => {
|
cp.stderr.on('data', (data) => {
|
||||||
if (logFileExist) {
|
if (logFileExist) {
|
||||||
fs.appendFileSync(`${absolutePath}`, `${data}`);
|
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cp.on('error', (err) => {
|
cp.on('error', (err) => {
|
||||||
@@ -417,12 +409,12 @@ export default class CronService {
|
|||||||
|
|
||||||
public async disabled(ids: number[]) {
|
public async disabled(ids: number[]) {
|
||||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||||
await this.set_crontab(true);
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
public async enabled(ids: number[]) {
|
||||||
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||||
await this.set_crontab(true);
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(id: number) {
|
public async log(id: number) {
|
||||||
@@ -435,35 +427,17 @@ export default class CronService {
|
|||||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||||
if (logFileExist) {
|
if (logFileExist) {
|
||||||
return getFileContentByName(`${absolutePath}`);
|
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 {
|
} else {
|
||||||
return '';
|
return '任务未运行或运行失败,请尝试手动运行';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logs(id: number) {
|
public async logs(id: number) {
|
||||||
const doc = await this.getDb({ id });
|
const doc = await this.getDb({ id });
|
||||||
if (!doc) {
|
if (!doc || !doc.log_path) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.log_path) {
|
|
||||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||||
const dir = path.resolve(config.logPath, relativeDir);
|
const dir = path.resolve(config.logPath, relativeDir);
|
||||||
if (existsSync(dir)) {
|
if (existsSync(dir)) {
|
||||||
@@ -475,64 +449,24 @@ export default class CronService {
|
|||||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.time - a.time);
|
.sort((a, b) => b.time - a.time);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, commandStr, url] = doc.command.split(/ +/);
|
|
||||||
let logPath = this.getKey(commandStr);
|
|
||||||
const isQlCommand = doc.command.startsWith('ql ');
|
|
||||||
const key =
|
|
||||||
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
|
|
||||||
logPath;
|
|
||||||
if (isQlCommand) {
|
|
||||||
logPath = 'update';
|
|
||||||
}
|
|
||||||
let logDir = `${config.logPath}${logPath}`;
|
|
||||||
if (existsSync(logDir)) {
|
|
||||||
let files = await promises.readdir(logDir);
|
|
||||||
if (isQlCommand) {
|
|
||||||
files = files.filter((x) => x.includes(key));
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
.map((x) => ({
|
|
||||||
filename: x,
|
|
||||||
directory: logPath,
|
|
||||||
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => b.time - a.time);
|
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private getKey(command: string): string {
|
|
||||||
const start =
|
|
||||||
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
|
||||||
const end =
|
|
||||||
command.lastIndexOf('.') !== -1
|
|
||||||
? command.lastIndexOf('.')
|
|
||||||
: command.length;
|
|
||||||
|
|
||||||
const tmpStr = command.substring(0, start - 1);
|
|
||||||
let index = 0;
|
|
||||||
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
|
|
||||||
index = tmpStr.lastIndexOf('/');
|
|
||||||
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
|
|
||||||
index = tmpStr.lastIndexOf(':');
|
|
||||||
}
|
|
||||||
if (index) {
|
|
||||||
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
|
|
||||||
} else {
|
|
||||||
return command.substring(start, end);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private make_command(tab: Crontab) {
|
private make_command(tab: Crontab) {
|
||||||
|
if (
|
||||||
|
!tab.command.startsWith(TASK_PREFIX) &&
|
||||||
|
!tab.command.startsWith(QL_PREFIX)
|
||||||
|
) {
|
||||||
|
tab.command = `${TASK_PREFIX}${tab.command}`;
|
||||||
|
}
|
||||||
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
||||||
return crontab_job_string;
|
return crontab_job_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async set_crontab(needReloadSchedule: boolean = false) {
|
private async set_crontab() {
|
||||||
const tabs = await this.crontabs();
|
const tabs = await this.crontabs();
|
||||||
var crontab_string = '';
|
var crontab_string = '';
|
||||||
tabs.data.forEach((tab) => {
|
tabs.data.forEach((tab) => {
|
||||||
@@ -555,9 +489,7 @@ export default class CronService {
|
|||||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||||
|
|
||||||
execSync(`crontab ${config.crontabFile}`);
|
execSync(`crontab ${config.crontabFile}`);
|
||||||
if (needReloadSchedule) {
|
|
||||||
exec(`pm2 reload schedule`);
|
exec(`pm2 reload schedule`);
|
||||||
}
|
|
||||||
await CrontabModel.update({ saved: true }, { where: {} });
|
await CrontabModel.update({ saved: true }, { where: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
||||||
import { initEnvPosition } from '../data/env';
|
import {
|
||||||
|
initPosition,
|
||||||
|
maxPosition,
|
||||||
|
minPosition,
|
||||||
|
stepPosition,
|
||||||
|
} from '../data/env';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronViewService {
|
export default class CronViewService {
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
public async create(payload: CrontabView): Promise<CrontabView> {
|
public async create(payload: CrontabView): Promise<CrontabView> {
|
||||||
let position = initEnvPosition;
|
let position = initPosition;
|
||||||
const views = await this.list();
|
const views = await this.list();
|
||||||
if (views && views.length > 0 && views[views.length - 1].position) {
|
if (views && views.length > 0 && views[views.length - 1].position) {
|
||||||
position = views[views.length - 1].position as number;
|
position = views[views.length - 1].position as number;
|
||||||
@@ -16,6 +21,8 @@ export default class CronViewService {
|
|||||||
position = position / 2;
|
position = position / 2;
|
||||||
const tab = new CrontabView({ ...payload, position });
|
const tab = new CrontabView({ ...payload, position });
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
|
|
||||||
|
await this.checkPosition(tab.position!);
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +69,22 @@ export default class CronViewService {
|
|||||||
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
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({
|
public async move({
|
||||||
id,
|
id,
|
||||||
fromIndex,
|
fromIndex,
|
||||||
@@ -76,17 +99,19 @@ export default class CronViewService {
|
|||||||
const views = await this.list();
|
const views = await this.list();
|
||||||
if (toIndex === 0 || toIndex === views.length - 1) {
|
if (toIndex === 0 || toIndex === views.length - 1) {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? views[0].position * 2
|
? views[0].position! * 2
|
||||||
: views[toIndex].position / 2;
|
: views[toIndex].position! / 2;
|
||||||
} else {
|
} else {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? (views[toIndex].position + views[toIndex - 1].position) / 2
|
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
|
||||||
: (views[toIndex].position + views[toIndex + 1].position) / 2;
|
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
|
||||||
}
|
}
|
||||||
const newDoc = await this.update({
|
const newDoc = await this.update({
|
||||||
id,
|
id,
|
||||||
position: targetPosition,
|
position: this.getPrecisionPosition(targetPosition),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.checkPosition(targetPosition);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
unInstallDependenceCommandTypes,
|
unInstallDependenceCommandTypes,
|
||||||
DependenceModel,
|
DependenceModel,
|
||||||
} from '../data/dependence';
|
} from '../data/dependence';
|
||||||
import _ from 'lodash';
|
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
@@ -125,8 +124,11 @@ export default class DependenceService {
|
|||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async find(query: any, sort?: any): Promise<Dependence[]> {
|
private async find(query: any, sort: any = []): Promise<Dependence[]> {
|
||||||
const docs = await DependenceModel.findAll({ where: { ...query } });
|
const docs = await DependenceModel.findAll({
|
||||||
|
where: { ...query },
|
||||||
|
order: [...sort, ['createdAt', 'DESC']],
|
||||||
|
});
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+57
-28
@@ -1,10 +1,17 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { getFileContentByName } from '../config/util';
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
|
import {
|
||||||
import _ from 'lodash';
|
Env,
|
||||||
|
EnvModel,
|
||||||
|
EnvStatus,
|
||||||
|
initPosition,
|
||||||
|
maxPosition,
|
||||||
|
minPosition,
|
||||||
|
stepPosition,
|
||||||
|
} from '../data/env';
|
||||||
|
import groupBy from 'lodash/groupBy';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -13,17 +20,22 @@ export default class EnvService {
|
|||||||
|
|
||||||
public async create(payloads: Env[]): Promise<Env[]> {
|
public async create(payloads: Env[]): Promise<Env[]> {
|
||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
let position = initEnvPosition;
|
let position = initPosition;
|
||||||
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
|
if (
|
||||||
position = envs[envs.length - 1].position as number;
|
envs &&
|
||||||
|
envs.length > 0 &&
|
||||||
|
typeof envs[envs.length - 1].position === 'number'
|
||||||
|
) {
|
||||||
|
position = envs[envs.length - 1].position!;
|
||||||
}
|
}
|
||||||
const tabs = payloads.map((x) => {
|
const tabs = payloads.map((x) => {
|
||||||
position = position / 2;
|
position = position - stepPosition;
|
||||||
const tab = new Env({ ...x, position });
|
const tab = new Env({ ...x, position });
|
||||||
return tab;
|
return tab;
|
||||||
});
|
});
|
||||||
const docs = await this.insert(tabs);
|
const docs = await this.insert(tabs);
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
|
await this.checkPosition(tabs[tabs.length - 1].position!);
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,25 +79,40 @@ export default class EnvService {
|
|||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
if (toIndex === 0 || toIndex === envs.length - 1) {
|
if (toIndex === 0 || toIndex === envs.length - 1) {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? envs[0].position * 2
|
? envs[0].position! + stepPosition
|
||||||
: envs[toIndex].position / 2;
|
: envs[toIndex].position! - stepPosition;
|
||||||
} else {
|
} else {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
|
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
|
||||||
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDoc = await this.update({
|
const newDoc = await this.update({
|
||||||
id,
|
id,
|
||||||
position: targetPosition,
|
position: this.getPrecisionPosition(targetPosition),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.checkPosition(targetPosition);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async envs(
|
private async checkPosition(position: number) {
|
||||||
searchText: string = '',
|
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||||
sort: any = { position: -1 },
|
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
|
||||||
query: any = {},
|
const envs = await this.envs();
|
||||||
): Promise<Env[]> {
|
let position = initPosition;
|
||||||
|
for (const env of envs) {
|
||||||
|
position = position - stepPosition;
|
||||||
|
await this.updateDb({ id: env.id, position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPrecisionPosition(position: number): number {
|
||||||
|
return parseFloat(position.toPrecision(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
|
||||||
let condition = { ...query };
|
let condition = { ...query };
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const encodeText = encodeURIComponent(searchText);
|
const encodeText = encodeURIComponent(searchText);
|
||||||
@@ -112,7 +139,11 @@ export default class EnvService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition, [['position', 'DESC']]);
|
const result = await this.find(condition, [
|
||||||
|
['status', 'ASC'],
|
||||||
|
['position', 'DESC'],
|
||||||
|
['createdAt', 'ASC'],
|
||||||
|
]);
|
||||||
return result as any;
|
return result as any;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -151,22 +182,20 @@ export default class EnvService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async set_envs() {
|
public async set_envs() {
|
||||||
const envs = await this.envs(
|
const envs = await this.envs('', {
|
||||||
'',
|
name: { [Op.not]: null },
|
||||||
{ position: -1 },
|
status: EnvStatus.normal,
|
||||||
{ name: { [Op.not]: null } },
|
});
|
||||||
);
|
const groups = groupBy(envs, 'name');
|
||||||
const groups = _.groupBy(envs, 'name');
|
|
||||||
let env_string = '';
|
let env_string = '';
|
||||||
for (const key in groups) {
|
for (const key in groups) {
|
||||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||||
const group = groups[key];
|
const group = groups[key];
|
||||||
|
|
||||||
// 忽略不符合bash要求的环境变量名称
|
// 忽略不符合bash要求的环境变量名称
|
||||||
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
|
||||||
let value = _(group)
|
let value = group
|
||||||
.filter((x) => x.status !== EnvStatus.disabled)
|
.map((x) => x.value)
|
||||||
.map('value')
|
|
||||||
.join('&')
|
.join('&')
|
||||||
.replace(/(\\)[^n]/g, '\\\\')
|
.replace(/(\\)[^n]/g, '\\\\')
|
||||||
.replace(/(\\$)/, '\\\\')
|
.replace(/(\\$)/, '\\\\')
|
||||||
|
|||||||
+150
-27
@@ -6,6 +6,7 @@ import got from 'got';
|
|||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
import { parseBody, parseHeaders } from '../config/util';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class NotificationService {
|
export default class NotificationService {
|
||||||
@@ -17,20 +18,27 @@ export default class NotificationService {
|
|||||||
['goCqHttpBot', this.goCqHttpBot],
|
['goCqHttpBot', this.goCqHttpBot],
|
||||||
['serverChan', this.serverChan],
|
['serverChan', this.serverChan],
|
||||||
['pushDeer', this.pushDeer],
|
['pushDeer', this.pushDeer],
|
||||||
|
['chat', this.chat],
|
||||||
['bark', this.bark],
|
['bark', this.bark],
|
||||||
['telegramBot', this.telegramBot],
|
['telegramBot', this.telegramBot],
|
||||||
['dingtalkBot', this.dingtalkBot],
|
['dingtalkBot', this.dingtalkBot],
|
||||||
['weWorkBot', this.weWorkBot],
|
['weWorkBot', this.weWorkBot],
|
||||||
['weWorkApp', this.weWorkApp],
|
['weWorkApp', this.weWorkApp],
|
||||||
|
['aibotk', this.aibotk],
|
||||||
['iGot', this.iGot],
|
['iGot', this.iGot],
|
||||||
['pushPlus', this.pushPlus],
|
['pushPlus', this.pushPlus],
|
||||||
['email', this.email],
|
['email', this.email],
|
||||||
|
['webhook', this.webhook],
|
||||||
|
['lark', this.lark],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
private timeout = 10000;
|
|
||||||
private title = '';
|
private title = '';
|
||||||
private content = '';
|
private content = '';
|
||||||
private params!: Omit<NotificationInfo, 'type'>;
|
private params!: Omit<NotificationInfo, 'type'>;
|
||||||
|
private gotOption = {
|
||||||
|
timeout: 30000,
|
||||||
|
retry: 1,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
@@ -73,8 +81,7 @@ export default class NotificationService {
|
|||||||
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
body: `title=${encodeURIComponent(
|
body: `title=${encodeURIComponent(
|
||||||
this.title,
|
this.title,
|
||||||
)}&message=${encodeURIComponent(
|
)}&message=${encodeURIComponent(
|
||||||
@@ -92,8 +99,7 @@ export default class NotificationService {
|
|||||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
json: { message: `${this.title}\n${this.content}` },
|
json: { message: `${this.title}\n${this.content}` },
|
||||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||||
})
|
})
|
||||||
@@ -108,8 +114,7 @@ export default class NotificationService {
|
|||||||
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
body: `title=${this.title}&desp=${this.content}`,
|
body: `title=${this.title}&desp=${this.content}`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
@@ -118,12 +123,11 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async pushDeer() {
|
private async pushDeer() {
|
||||||
const { pushDeerKey } = this.params;
|
const { pushDeerKey, pushDeerUrl } = this.params;
|
||||||
const url = `https://api2.pushdeer.com/message/push`;
|
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
||||||
this.title,
|
this.title,
|
||||||
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
||||||
@@ -135,6 +139,19 @@ export default class NotificationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async chat() {
|
||||||
|
const { chatUrl, chatToken } = this.params;
|
||||||
|
const url = `${chatUrl}${chatToken}`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.success;
|
||||||
|
}
|
||||||
|
|
||||||
private async bark() {
|
private async bark() {
|
||||||
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
||||||
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
||||||
@@ -147,8 +164,7 @@ export default class NotificationService {
|
|||||||
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
|
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.get(url, {
|
.get(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
@@ -186,8 +202,7 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
agent,
|
agent,
|
||||||
@@ -209,8 +224,7 @@ export default class NotificationService {
|
|||||||
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
@@ -227,8 +241,7 @@ export default class NotificationService {
|
|||||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
@@ -247,8 +260,7 @@ export default class NotificationService {
|
|||||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
||||||
const tokenRes: any = await got
|
const tokenRes: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
json: {
|
json: {
|
||||||
corpid,
|
corpid,
|
||||||
corpsecret,
|
corpsecret,
|
||||||
@@ -298,8 +310,7 @@ export default class NotificationService {
|
|||||||
.post(
|
.post(
|
||||||
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
||||||
{
|
{
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
json: {
|
json: {
|
||||||
touser,
|
touser,
|
||||||
agentid,
|
agentid,
|
||||||
@@ -313,13 +324,53 @@ export default class NotificationService {
|
|||||||
return res.errcode === 0;
|
return res.errcode === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async aibotk() {
|
||||||
|
const { aibotkKey, aibotkType, aibotkName } = this.params;
|
||||||
|
let url = '';
|
||||||
|
let json = {};
|
||||||
|
switch (aibotkType) {
|
||||||
|
case 'room':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
|
||||||
|
json = {
|
||||||
|
apiKey: `${aibotkKey}`,
|
||||||
|
roomName: `${aibotkName}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'contact':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
|
||||||
|
json = {
|
||||||
|
apiKey: `${aibotkKey}`,
|
||||||
|
name: `${aibotkName}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
...json,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.code === 0;
|
||||||
|
}
|
||||||
|
|
||||||
private async iGot() {
|
private async iGot() {
|
||||||
const { iGotPushKey } = this.params;
|
const { iGotPushKey } = this.params;
|
||||||
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
body: `title=${this.title}&content=${this.content}`,
|
body: `title=${this.title}&content=${this.content}`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
@@ -333,8 +384,7 @@ export default class NotificationService {
|
|||||||
const url = `https://www.pushplus.plus/send`;
|
const url = `https://www.pushplus.plus/send`;
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
timeout: this.timeout,
|
...this.gotOption,
|
||||||
retry: 0,
|
|
||||||
json: {
|
json: {
|
||||||
token: `${pushPlusToken}`,
|
token: `${pushPlusToken}`,
|
||||||
title: `${this.title}`,
|
title: `${this.title}`,
|
||||||
@@ -347,6 +397,21 @@ export default class NotificationService {
|
|||||||
return res.code === 200;
|
return res.code === 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async lark() {
|
||||||
|
const { larkKey } = this.params;
|
||||||
|
const res: any = await got
|
||||||
|
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
msg_type: 'text',
|
||||||
|
content: { text: `${this.title}\n\n${this.content}` },
|
||||||
|
},
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.StatusCode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
private async email() {
|
private async email() {
|
||||||
const { emailPass, emailService, emailUser } = this.params;
|
const { emailPass, emailService, emailUser } = this.params;
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
@@ -368,4 +433,62 @@ export default class NotificationService {
|
|||||||
|
|
||||||
return !!info.messageId;
|
return !!info.messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async webhook() {
|
||||||
|
const {
|
||||||
|
webhookUrl,
|
||||||
|
webhookBody,
|
||||||
|
webhookHeaders,
|
||||||
|
webhookMethod,
|
||||||
|
webhookContentType,
|
||||||
|
} = this.params;
|
||||||
|
|
||||||
|
const { formatBody, formatUrl } = this.formatNotifyContent(
|
||||||
|
webhookUrl,
|
||||||
|
webhookBody,
|
||||||
|
);
|
||||||
|
if (!formatUrl && !formatBody) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const headers = parseHeaders(webhookHeaders);
|
||||||
|
const body = parseBody(formatBody, webhookContentType);
|
||||||
|
const bodyParam = this.formatBody(webhookContentType, body);
|
||||||
|
const options = {
|
||||||
|
method: webhookMethod,
|
||||||
|
headers,
|
||||||
|
...this.gotOption,
|
||||||
|
allowGetBody: true,
|
||||||
|
...bodyParam,
|
||||||
|
};
|
||||||
|
const res = await got(formatUrl, options);
|
||||||
|
return String(res.statusCode).startsWith('20');
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatBody(contentType: string, body: any): object {
|
||||||
|
if (!body) return {};
|
||||||
|
switch (contentType) {
|
||||||
|
case 'application/json':
|
||||||
|
return { json: body };
|
||||||
|
case 'multipart/form-data':
|
||||||
|
return { form: body };
|
||||||
|
case 'application/x-www-form-urlencoded':
|
||||||
|
return { body };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatNotifyContent(url: string, body: string) {
|
||||||
|
if (!url.includes('$title') && !body.includes('$title')) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatUrl: url
|
||||||
|
.replaceAll('$title', encodeURIComponent(this.title))
|
||||||
|
.replaceAll('$content', encodeURIComponent(this.content)),
|
||||||
|
formatBody: body
|
||||||
|
.replaceAll('$title', this.title)
|
||||||
|
.replaceAll('$content', this.content),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default class OpenService {
|
|||||||
(x) => x.expiration >= timestamp,
|
(x) => x.expiration >= timestamp,
|
||||||
);
|
);
|
||||||
let tokens = invalidTokens;
|
let tokens = invalidTokens;
|
||||||
if (invalidTokens.length > 5) {
|
if (invalidTokens.length >= 5) {
|
||||||
tokens = [
|
tokens = [
|
||||||
...invalidTokens.slice(0, 4),
|
...invalidTokens.slice(0, 4),
|
||||||
{ ...invalidTokens[4], expiration },
|
{ ...invalidTokens[4], expiration },
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ export default class ScheduleService {
|
|||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
async runTask(command: string, callbacks: TaskCallbacks = {}) {
|
async runTask(
|
||||||
|
command: string,
|
||||||
|
callbacks: TaskCallbacks = {},
|
||||||
|
completionTime: 'start' | 'end' = 'end',
|
||||||
|
) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const startTime = dayjs();
|
const startTime = dayjs();
|
||||||
@@ -52,6 +56,7 @@ export default class ScheduleService {
|
|||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
callbacks.onStart?.(cp, startTime);
|
callbacks.onStart?.(cp, startTime);
|
||||||
|
completionTime === 'start' && resolve(cp.pid);
|
||||||
|
|
||||||
cp.stdout.on('data', async (data) => {
|
cp.stdout.on('data', async (data) => {
|
||||||
await callbacks.onLog?.(data.toString());
|
await callbacks.onLog?.(data.toString());
|
||||||
@@ -100,7 +105,6 @@ export default class ScheduleService {
|
|||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
await callbacks.onError?.(JSON.stringify(error));
|
await callbacks.onError?.(JSON.stringify(error));
|
||||||
resolve(null);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-13
@@ -6,6 +6,8 @@ import SockService from './sock';
|
|||||||
import CronService from './cron';
|
import CronService from './cron';
|
||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { TASK_COMMAND } from '../config/const';
|
||||||
|
import { getPid, killTask } from '../config/util';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ScriptService {
|
export default class ScriptService {
|
||||||
@@ -40,23 +42,25 @@ export default class ScriptService {
|
|||||||
|
|
||||||
public async runScript(filePath: string) {
|
public async runScript(filePath: string) {
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
const command = `task -l ${relativePath} now`;
|
const command = `${TASK_COMMAND} -l ${relativePath} now`;
|
||||||
this.scheduleService.runTask(command, this.taskCallbacks(filePath));
|
const pid = await this.scheduleService.runTask(
|
||||||
|
command,
|
||||||
|
this.taskCallbacks(filePath),
|
||||||
|
'start',
|
||||||
|
);
|
||||||
|
|
||||||
return { code: 200 };
|
return { code: 200, data: pid };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stopScript(filePath: string) {
|
public async stopScript(filePath: string, pid: number) {
|
||||||
|
let str = '';
|
||||||
|
if (!pid) {
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
|
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
|
||||||
|
}
|
||||||
const str = err ? `\n${err}` : '';
|
try {
|
||||||
this.sockService.sendMessage({
|
await killTask(pid);
|
||||||
type: 'manuallyRunScript',
|
} catch (error) {}
|
||||||
message: `${str}\n## 执行结束... ${new Date()
|
|
||||||
.toLocaleString('zh', { hour12: false })
|
|
||||||
.replace(' 24:', ' 00:')} `,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-7
@@ -14,7 +14,7 @@ export default class SshKeyService {
|
|||||||
|
|
||||||
private generatePrivateKeyFile(alias: string, key: string): void {
|
private generatePrivateKeyFile(alias: string, key: string): void {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
|
fs.writeFileSync(`${this.sshPath}/${alias}`, `${key}${os.EOL}`, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
mode: '400',
|
mode: '400',
|
||||||
});
|
});
|
||||||
@@ -38,8 +38,16 @@ export default class SshKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateSingleSshConfig(alias: string, host: string): string {
|
private generateSingleSshConfig(
|
||||||
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
|
alias: string,
|
||||||
|
host: string,
|
||||||
|
proxy?: string,
|
||||||
|
): string {
|
||||||
|
if (host === 'github.com') {
|
||||||
|
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
|
||||||
|
}
|
||||||
|
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
|
||||||
|
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateSshConfig(configs: string[]) {
|
private generateSshConfig(configs: string[]) {
|
||||||
@@ -69,16 +77,21 @@ export default class SshKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addSSHKey(key: string, alias: string, host: string): void {
|
public addSSHKey(
|
||||||
|
key: string,
|
||||||
|
alias: string,
|
||||||
|
host: string,
|
||||||
|
proxy?: string,
|
||||||
|
): void {
|
||||||
this.generatePrivateKeyFile(alias, key);
|
this.generatePrivateKeyFile(alias, key);
|
||||||
const config = this.generateSingleSshConfig(alias, host);
|
const config = this.generateSingleSshConfig(alias, host, proxy);
|
||||||
this.removeSshConfig(alias);
|
this.removeSshConfig(alias);
|
||||||
this.generateSshConfig([config]);
|
this.generateSshConfig([config]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeSSHKey(alias: string, host: string): void {
|
public removeSSHKey(alias: string, host: string, proxy?: string): void {
|
||||||
this.removePrivateKeyFile(alias);
|
this.removePrivateKeyFile(alias);
|
||||||
const config = this.generateSingleSshConfig(alias, host);
|
const config = this.generateSingleSshConfig(alias, host, proxy);
|
||||||
this.removeSshConfig(config);
|
this.removeSshConfig(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,14 @@ import {
|
|||||||
spawn,
|
spawn,
|
||||||
} from 'child_process';
|
} from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import cron_parser from 'cron-parser';
|
|
||||||
import {
|
import {
|
||||||
getFileContentByName,
|
getFileContentByName,
|
||||||
concurrentRun,
|
concurrentRun,
|
||||||
fileExist,
|
fileExist,
|
||||||
createFile,
|
createFile,
|
||||||
|
killTask,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import { promises, existsSync } from 'fs';
|
import { promises, existsSync } from 'fs';
|
||||||
import { promisify } from 'util';
|
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
@@ -29,6 +28,7 @@ import { SimpleIntervalSchedule } from 'toad-scheduler';
|
|||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import SshKeyService from './sshKey';
|
import SshKeyService from './sshKey';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { LOG_END_SYMBOL } from '../config/const';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionService {
|
export default class SubscriptionService {
|
||||||
@@ -62,7 +62,10 @@ export default class SubscriptionService {
|
|||||||
try {
|
try {
|
||||||
const result = await SubscriptionModel.findAll({
|
const result = await SubscriptionModel.findAll({
|
||||||
where: query,
|
where: query,
|
||||||
order: [['createdAt', 'DESC']],
|
order: [
|
||||||
|
['is_disabled', 'ASC'],
|
||||||
|
['createdAt', 'DESC'],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
return result as any;
|
return result as any;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -73,13 +76,25 @@ export default class SubscriptionService {
|
|||||||
private formatCommand(doc: Subscription, url?: string) {
|
private formatCommand(doc: Subscription, url?: string) {
|
||||||
let command = 'ql ';
|
let command = 'ql ';
|
||||||
let _url = url || this.formatUrl(doc).url;
|
let _url = url || this.formatUrl(doc).url;
|
||||||
const { type, whitelist, blacklist, dependences, branch, extensions } = doc;
|
const {
|
||||||
|
type,
|
||||||
|
whitelist,
|
||||||
|
blacklist,
|
||||||
|
dependences,
|
||||||
|
branch,
|
||||||
|
extensions,
|
||||||
|
proxy,
|
||||||
|
autoAddCron,
|
||||||
|
autoDelCron,
|
||||||
|
} = doc;
|
||||||
if (type === 'file') {
|
if (type === 'file') {
|
||||||
command += `raw "${_url}"`;
|
command += `raw "${_url}"`;
|
||||||
} else {
|
} else {
|
||||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
||||||
dependences || ''
|
dependences || ''
|
||||||
}" "${branch || ''}" "${extensions || ''}"`;
|
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${
|
||||||
|
Boolean(autoAddCron) || ''
|
||||||
|
}" "${Boolean(autoDelCron) || ''}"`;
|
||||||
}
|
}
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
@@ -113,9 +128,10 @@ export default class SubscriptionService {
|
|||||||
(doc.pull_option as any).private_key,
|
(doc.pull_option as any).private_key,
|
||||||
doc.alias,
|
doc.alias,
|
||||||
host,
|
host,
|
||||||
|
doc.proxy,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.sshKeyService.removeSSHKey(doc.alias, host);
|
this.sshKeyService.removeSSHKey(doc.alias, host, doc.proxy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +245,7 @@ export default class SubscriptionService {
|
|||||||
absolutePath,
|
absolutePath,
|
||||||
`\n## 执行结束... ${endTime.format(
|
`\n## 执行结束... ${endTime.format(
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
)} 耗时 ${diff} 秒`,
|
)} 耗时 ${diff} 秒${LOG_END_SYMBOL}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await SubscriptionModel.update(
|
await SubscriptionModel.update(
|
||||||
@@ -268,7 +284,8 @@ export default class SubscriptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Subscription): Promise<Subscription> {
|
public async update(payload: Subscription): Promise<Subscription> {
|
||||||
const newDoc = await this.updateDb(payload);
|
const tab = new Subscription(payload);
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
await this.handleTask(newDoc, !newDoc.is_disabled);
|
await this.handleTask(newDoc, !newDoc.is_disabled);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
@@ -338,19 +355,18 @@ export default class SubscriptionService {
|
|||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
if (doc.pid) {
|
if (doc.pid) {
|
||||||
try {
|
try {
|
||||||
process.kill(-doc.pid);
|
await killTask(doc.pid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.silly(error);
|
this.logger.silly(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const command = this.formatCommand(doc);
|
|
||||||
const err = await this.killTask(command);
|
|
||||||
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
||||||
const str = err ? `\n${err}` : '';
|
|
||||||
|
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
`${absolutePath}`,
|
`${absolutePath}`,
|
||||||
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `,
|
`\n## 执行结束... ${dayjs().format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)}${LOG_END_SYMBOL}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,41 +376,6 @@ 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) {
|
private async runSingle(subscriptionId: number) {
|
||||||
const subscription = await this.getDb({ id: subscriptionId });
|
const subscription = await this.getDb({ id: subscriptionId });
|
||||||
if (subscription.status !== SubscriptionStatus.queued) {
|
if (subscription.status !== SubscriptionStatus.queued) {
|
||||||
|
|||||||
+22
-26
@@ -2,7 +2,6 @@ import { Service, Inject } from 'typedi';
|
|||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import _ from 'lodash';
|
|
||||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||||
import { NotificationInfo } from '../data/notify';
|
import { NotificationInfo } from '../data/notify';
|
||||||
import NotificationService from './notify';
|
import NotificationService from './notify';
|
||||||
@@ -10,6 +9,7 @@ import ScheduleService from './schedule';
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
|
import { parseContentVersion, parseVersion } from '../config/util';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SystemService {
|
export default class SystemService {
|
||||||
@@ -70,9 +70,8 @@ export default class SystemService {
|
|||||||
};
|
};
|
||||||
await this.scheduleService.cancelIntervalTask(cron);
|
await this.scheduleService.cancelIntervalTask(cron);
|
||||||
if (frequency > 0) {
|
if (frequency > 0) {
|
||||||
await this.scheduleService.createIntervalTask(cron, {
|
this.scheduleService.createIntervalTask(cron, {
|
||||||
days: frequency,
|
days: frequency,
|
||||||
runImmediately: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { code: 200, data: { ...cron } };
|
return { code: 200, data: { ...cron } };
|
||||||
@@ -80,32 +79,33 @@ export default class SystemService {
|
|||||||
|
|
||||||
public async checkUpdate() {
|
public async checkUpdate() {
|
||||||
try {
|
try {
|
||||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
const currentVersionContent = await parseVersion(config.versionFile);
|
||||||
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
|
||||||
|
|
||||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
let lastVersionContent;
|
||||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
|
||||||
|
|
||||||
let lastVersion = '';
|
|
||||||
let lastLog = '';
|
|
||||||
try {
|
try {
|
||||||
const result = await got.get(config.lastVersionFile, {
|
const result = await got.get(
|
||||||
timeout: 6000,
|
`${config.lastVersionFile}?t=${Date.now()}`,
|
||||||
retry: 0,
|
{
|
||||||
});
|
timeout: 30000,
|
||||||
const lastVersionFileContent = result.body;
|
},
|
||||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
);
|
||||||
lastLog = lastVersionFileContent.match(logRegx)
|
lastVersionContent = await parseContentVersion(result.body);
|
||||||
? lastVersionFileContent.match(logRegx)![1]
|
|
||||||
: '';
|
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
|
if (!lastVersionContent) {
|
||||||
|
lastVersionContent = currentVersionContent;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data: {
|
data: {
|
||||||
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
hasNewVersion: this.checkHasNewVersion(
|
||||||
lastVersion,
|
currentVersionContent.version,
|
||||||
lastLog,
|
lastVersionContent.version,
|
||||||
|
),
|
||||||
|
lastVersion: lastVersionContent.version,
|
||||||
|
lastLog: lastVersionContent.changeLog,
|
||||||
|
lastLogLink: lastVersionContent.changeLogLink,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -138,10 +138,6 @@ export default class SystemService {
|
|||||||
public async updateSystem() {
|
public async updateSystem() {
|
||||||
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
||||||
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'updateSystemVersion',
|
|
||||||
message: `开始更新系统`,
|
|
||||||
});
|
|
||||||
cp.stdout.on('data', (data) => {
|
cp.stdout.on('data', (data) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: 'updateSystemVersion',
|
type: 'updateSystemVersion',
|
||||||
|
|||||||
+26
-13
@@ -3,7 +3,6 @@ import winston from 'winston';
|
|||||||
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import _ from 'lodash';
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { authenticator } from '@otplib/preset-default';
|
import { authenticator } from '@otplib/preset-default';
|
||||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||||
@@ -65,32 +64,37 @@ export default class UserService {
|
|||||||
return this.initAuthInfo();
|
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 {
|
return {
|
||||||
code: 410,
|
code: 410,
|
||||||
message: `失败次数过多,请${Math.round(
|
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||||
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
data: waitTime,
|
||||||
)}秒后重试`,
|
|
||||||
data: Math.round(
|
|
||||||
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ip, address } = await getNetIp(req);
|
if (
|
||||||
if (username === cUsername && password === cPassword) {
|
username === cUsername &&
|
||||||
if (twoFactorActivated && needTwoFactor) {
|
password === cPassword &&
|
||||||
|
twoFactorActivated &&
|
||||||
|
needTwoFactor
|
||||||
|
) {
|
||||||
this.updateAuthInfo(content, {
|
this.updateAuthInfo(content, {
|
||||||
isTwoFactorChecking: true,
|
isTwoFactorChecking: true,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
code: 420,
|
code: 420,
|
||||||
message: '请输入两步验证token',
|
message: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { ip, address } = await getNetIp(req);
|
||||||
|
if (username === cUsername && password === cPassword) {
|
||||||
const data = createRandomString(50, 100);
|
const data = createRandomString(50, 100);
|
||||||
const expiration = twoFactorActivated ? 30 : 3;
|
const expiration = twoFactorActivated ? 60 : 20;
|
||||||
let token = jwt.sign({ data }, config.secret as any, {
|
let token = jwt.sign({ data }, config.secret as any, {
|
||||||
expiresIn: 60 * 60 * 24 * expiration,
|
expiresIn: 60 * 60 * 24 * expiration,
|
||||||
algorithm: 'HS384',
|
algorithm: 'HS384',
|
||||||
@@ -155,8 +159,17 @@ export default class UserService {
|
|||||||
status: LoginStatus.fail,
|
status: LoginStatus.fail,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (retries > 2) {
|
||||||
|
const waitTime = Math.round(Math.pow(3, retries + 1));
|
||||||
|
return {
|
||||||
|
code: 410,
|
||||||
|
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||||
|
data: waitTime,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
return { code: 400, message: config.authError };
|
return { code: 400, message: config.authError };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return this.initAuthInfo();
|
return this.initAuthInfo();
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -5,6 +5,7 @@ import LoggerInstance from './loaders/logger';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
const tokenFile = path.join(config.configPath, 'token.json');
|
const tokenFile = path.join(config.configPath, 'token.json');
|
||||||
|
|
||||||
@@ -25,9 +26,14 @@ async function getToken() {
|
|||||||
|
|
||||||
async function writeFile(data: any) {
|
async function writeFile(data: any) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => {
|
fs.writeFile(
|
||||||
|
tokenFile,
|
||||||
|
`${JSON.stringify(data)}${os.EOL}`,
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
() => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-9
@@ -1,4 +1,13 @@
|
|||||||
FROM python: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 \
|
||||||
|
&& cd /tmp/build \
|
||||||
|
&& pnpm install --prod
|
||||||
|
|
||||||
|
FROM python:3.10-alpine
|
||||||
|
|
||||||
ARG QL_MAINTAINER="whyour"
|
ARG QL_MAINTAINER="whyour"
|
||||||
LABEL maintainer="${QL_MAINTAINER}"
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
@@ -13,8 +22,6 @@ ENV PNPM_HOME=/root/.local/share/pnpm \
|
|||||||
QL_DIR=/ql \
|
QL_DIR=/ql \
|
||||||
QL_BRANCH=${QL_BRANCH}
|
QL_BRANCH=${QL_BRANCH}
|
||||||
|
|
||||||
WORKDIR ${QL_DIR}
|
|
||||||
|
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||||
&& apk update -f \
|
&& apk update -f \
|
||||||
@@ -42,19 +49,24 @@ RUN set -x \
|
|||||||
&& git config --global http.postBuffer 524288000 \
|
&& git config --global http.postBuffer 524288000 \
|
||||||
&& npm install -g pnpm \
|
&& npm install -g pnpm \
|
||||||
&& pnpm add -g pm2 ts-node typescript tslib \
|
&& pnpm add -g pm2 ts-node typescript tslib \
|
||||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
&& rm -rf /root/.pnpm-store \
|
||||||
|
&& rm -rf /root/.local/share/pnpm/store \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& rm -rf /root/.npm
|
||||||
|
|
||||||
|
ARG SOURCE_COMMIT
|
||||||
|
RUN git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||||
&& cd ${QL_DIR} \
|
&& cd ${QL_DIR} \
|
||||||
&& cp -f .env.example .env \
|
&& cp -f .env.example .env \
|
||||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||||
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||||
&& 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 \
|
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||||
&& mkdir -p ${QL_DIR}/static \
|
&& mkdir -p ${QL_DIR}/static \
|
||||||
&& cp -rf /static/* ${QL_DIR}/static \
|
&& cp -rf /static/* ${QL_DIR}/static \
|
||||||
&& rm -rf /static
|
&& rm -rf /static
|
||||||
|
|
||||||
|
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ dir_shell=/ql/shell
|
|||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
link_shell
|
link_shell
|
||||||
|
|
||||||
|
export isFirstStartServer=true
|
||||||
|
|
||||||
echo -e "======================1. 检测配置文件========================\n"
|
echo -e "======================1. 检测配置文件========================\n"
|
||||||
make_dir /etc/nginx/conf.d
|
make_dir /etc/nginx/conf.d
|
||||||
make_dir /run/nginx
|
make_dir /run/nginx
|
||||||
@@ -11,6 +13,8 @@ cp -fv $nginx_conf /etc/nginx/nginx.conf
|
|||||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||||
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
|
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
|
||||||
pm2 l &>/dev/null
|
pm2 l &>/dev/null
|
||||||
|
|
||||||
|
patch_version &>/dev/null
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo -e "======================2. 安装依赖========================\n"
|
echo -e "======================2. 安装依赖========================\n"
|
||||||
@@ -38,13 +42,13 @@ echo -e "定时任务启动成功...\n"
|
|||||||
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
echo -e "======================7. 启动bot========================\n"
|
echo -e "======================7. 启动bot========================\n"
|
||||||
nohup ql bot >>$dir_log/start.log 2>&1 &
|
nohup ql bot >$dir_log/bot.log 2>&1 &
|
||||||
echo -e "bot后台启动中...\n"
|
echo -e "bot后台启动中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EnableExtraShell == true ]]; then
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
echo -e "======================8. 执行自定义脚本========================\n"
|
echo -e "======================8. 执行自定义脚本========================\n"
|
||||||
nohup ql extra >>$dir_log/start.log 2>&1 &
|
nohup ql extra >$dir_log/extra.log 2>&1 &
|
||||||
echo -e "自定义脚本后台执行中...\n"
|
echo -e "自定义脚本后台执行中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+50
-26
@@ -2,16 +2,16 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently -n w: npm:start:*",
|
"start": "concurrently -n w: npm:start:*",
|
||||||
"start:front": "umi dev",
|
"start:front": "max dev",
|
||||||
"start:back": "nodemon",
|
"start:back": "nodemon",
|
||||||
"start:public": "ts-node back/public.ts",
|
"start:public": "ts-node --transpile-only ./back/public.ts",
|
||||||
"build:front": "umi build",
|
"build:front": "max build",
|
||||||
"build:back": "tsc -p tsconfig.back.json",
|
"build:back": "tsc -p tsconfig.back.json",
|
||||||
"panel": "npm run build:back && node static/build/app.js",
|
"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.js",
|
||||||
"public": "npm run build:back && node static/build/public.js",
|
"public": "npm run build:back && node static/build/public.js",
|
||||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||||
"postinstall": "umi generate tmp 2>/dev/null || true",
|
"postinstall": "max setup 2>/dev/null || true",
|
||||||
"test": "umi-test",
|
"test": "umi-test",
|
||||||
"test:coverage": "umi-test --coverage"
|
"test:coverage": "umi-test --coverage"
|
||||||
},
|
},
|
||||||
@@ -28,16 +28,34 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"peerDependencyRules": {
|
"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": {
|
"allowedVersions": {
|
||||||
"react": "17",
|
"react": "18",
|
||||||
"react-dom": "17"
|
"react-dom": "18",
|
||||||
|
"dva-core": "2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@otplib/preset-default": "^12.0.1",
|
"@otplib/preset-default": "^12.0.1",
|
||||||
"@sentry/node": "^7.0.0",
|
"@sentry/node": "^7.12.1",
|
||||||
"@sentry/tracing": "^7.0.0",
|
"@sentry/tracing": "^7.12.1",
|
||||||
"body-parser": "^1.19.2",
|
"body-parser": "^1.19.2",
|
||||||
"celebrate": "^15.0.1",
|
"celebrate": "^15.0.1",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
@@ -48,21 +66,23 @@
|
|||||||
"express": "^4.17.3",
|
"express": "^4.17.3",
|
||||||
"express-jwt": "^6.1.1",
|
"express-jwt": "^6.1.1",
|
||||||
"express-urlrewrite": "^1.4.0",
|
"express-urlrewrite": "^1.4.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
"hpagent": "^0.1.2",
|
"hpagent": "^0.1.2",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"multer": "^1.4.4",
|
"multer": "^1.4.4",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.7.2",
|
||||||
"p-queue": "7.2.0",
|
"pstree.remy": "^1.1.8",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sequelize": "^6.20.1",
|
"sequelize": "^6.25.5",
|
||||||
"serve-handler": "^6.1.3",
|
"serve-handler": "^6.1.3",
|
||||||
"sockjs": "^0.3.24",
|
"sockjs": "^0.3.24",
|
||||||
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
|
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.2",
|
||||||
"toad-scheduler": "^1.6.0",
|
"toad-scheduler": "^1.6.0",
|
||||||
"typedi": "^0.10.0",
|
"typedi": "^0.10.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
@@ -71,57 +91,61 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^4.7.0",
|
"@ant-design/icons": "^4.7.0",
|
||||||
"@ant-design/pro-layout": "^6.33.1",
|
"@ant-design/pro-layout": "6.38.22",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@monaco-editor/react": "4.4.6",
|
||||||
"@sentry/react": "^7.0.0",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
|
"@sentry/react": "^7.12.1",
|
||||||
"@types/body-parser": "^1.19.2",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/express-jwt": "^6.0.4",
|
"@types/express-jwt": "^6.0.4",
|
||||||
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/jsonwebtoken": "^8.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/lodash": "^4.14.179",
|
"@types/lodash": "^4.14.185",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/nedb": "^1.8.12",
|
"@types/nedb": "^1.8.12",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/node-schedule": "^1.3.2",
|
"@types/node-schedule": "^1.3.2",
|
||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/qrcode.react": "^1.0.2",
|
"@types/qrcode.react": "^1.0.2",
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "^18.0.20",
|
||||||
"@types/react-dom": "^17.0.13",
|
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/serve-handler": "^6.1.1",
|
"@types/serve-handler": "^6.1.1",
|
||||||
"@types/sockjs": "^0.3.33",
|
"@types/sockjs": "^0.3.33",
|
||||||
"@types/sockjs-client": "^1.5.1",
|
"@types/sockjs-client": "^1.5.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@umijs/plugin-antd": "^0.15.0",
|
"@umijs/max": "^4.0.42",
|
||||||
"@umijs/plugin-esbuild": "^1.4.1",
|
|
||||||
"@umijs/ssr-darkreader": "^4.9.45",
|
"@umijs/ssr-darkreader": "^4.9.45",
|
||||||
"@umijs/test": "^3.5.21",
|
|
||||||
"ansi-to-react": "^6.1.6",
|
"ansi-to-react": "^6.1.6",
|
||||||
"antd": "^4.20.5",
|
"antd": "^4.24.7",
|
||||||
"antd-img-crop": "^4.2.3",
|
"antd-img-crop": "^4.2.3",
|
||||||
"codemirror": "^5.65.2",
|
"codemirror": "^5.65.2",
|
||||||
"compression-webpack-plugin": "9.2.0",
|
"compression-webpack-plugin": "9.2.0",
|
||||||
"concurrently": "^7.0.0",
|
"concurrently": "^7.0.0",
|
||||||
"lint-staged": "^12.3.4",
|
"lint-staged": "^13.0.3",
|
||||||
|
"monaco-editor": "0.33.0",
|
||||||
"nodemon": "^2.0.15",
|
"nodemon": "^2.0.15",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"qiniu": "^7.4.0",
|
"qiniu": "^7.4.0",
|
||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
|
"query-string": "^7.1.1",
|
||||||
"rc-tween-one": "^3.0.6",
|
"rc-tween-one": "^3.0.6",
|
||||||
"react": "17.x",
|
"react": "18.2.0",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react-codemirror2": "^7.2.1",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "^14.0.2",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^14.0.0",
|
||||||
"react-dom": "17.x",
|
"react-dom": "18.2.0",
|
||||||
"react-split-pane": "^0.1.92",
|
"react-split-pane": "^0.1.92",
|
||||||
"sockjs-client": "^1.6.0",
|
"sockjs-client": "^1.6.0",
|
||||||
"ts-node": "^10.6.0",
|
"ts-node": "^10.6.0",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "^4.6.2",
|
"typescript": "4.8.4",
|
||||||
"umi": "^3.5.21",
|
|
||||||
"umi-request": "^1.4.0",
|
"umi-request": "^1.4.0",
|
||||||
"vh-check": "^2.0.5",
|
"vh-check": "^2.0.5",
|
||||||
|
"virtuallist-antd": "^0.7.6",
|
||||||
"webpack": "^5.70.0",
|
"webpack": "^5.70.0",
|
||||||
"yorkie": "^2.0.0"
|
"yorkie": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+15239
File diff suppressed because it is too large
Load Diff
+39
-3
@@ -16,9 +16,14 @@ DefaultCronRule=""
|
|||||||
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
|
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
|
||||||
RepoFileExtensions="js py"
|
RepoFileExtensions="js py"
|
||||||
|
|
||||||
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
|
## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890
|
||||||
ProxyUrl=""
|
ProxyUrl=""
|
||||||
|
|
||||||
|
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
|
||||||
|
CpuWarn=80
|
||||||
|
MemoryWarn=80
|
||||||
|
DiskWarn=90
|
||||||
|
|
||||||
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||||
CommandTimeoutTime="1h"
|
CommandTimeoutTime="1h"
|
||||||
|
|
||||||
@@ -46,9 +51,12 @@ AutoStartBot=""
|
|||||||
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
|
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
|
||||||
BotRepoUrl=""
|
BotRepoUrl=""
|
||||||
|
|
||||||
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
## 安装python依赖时指定pip源
|
||||||
PipMirror="https://pypi.doubanio.com/simple/"
|
PipMirror="https://pypi.doubanio.com/simple/"
|
||||||
|
|
||||||
|
## 安装node依赖时指定npm源
|
||||||
|
NpmMirror="https://registry.npmmirror.com"
|
||||||
|
|
||||||
## 通知环境变量
|
## 通知环境变量
|
||||||
## 1. Server酱
|
## 1. Server酱
|
||||||
## https://sct.ftqq.com
|
## https://sct.ftqq.com
|
||||||
@@ -59,7 +67,7 @@ export PUSH_KEY=""
|
|||||||
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
|
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
|
||||||
export BARK_PUSH=""
|
export BARK_PUSH=""
|
||||||
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
|
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
|
||||||
export BARK_ICON="http://qn.whyour.cn/logo.png"
|
export BARK_ICON="https://qn.whyour.cn/logo.png"
|
||||||
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
|
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
|
||||||
export BARK_SOUND=""
|
export BARK_SOUND=""
|
||||||
## 下方填写推送消息分组,默认为"QingLong"
|
## 下方填写推送消息分组,默认为"QingLong"
|
||||||
@@ -137,4 +145,32 @@ export GOTIFY_PRIORITY=0
|
|||||||
## deer_key 填写PushDeer的key
|
## deer_key 填写PushDeer的key
|
||||||
export DEER_KEY=""
|
export DEER_KEY=""
|
||||||
|
|
||||||
|
## 12. Chat
|
||||||
|
## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token=
|
||||||
|
## chat_token 填写后面的token
|
||||||
|
export CHAT_URL=""
|
||||||
|
export CHAT_TOKEN=""
|
||||||
|
|
||||||
|
## 13. aibotk
|
||||||
|
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
|
||||||
|
## aibotk_key (必填)填写智能微秘书个人中心的apikey
|
||||||
|
export AIBOTK_KEY=""
|
||||||
|
## aibotk_type (必填)填写发送的目标 room 或 contact, 填其他的不生效
|
||||||
|
export AIBOTK_TYPE=""
|
||||||
|
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
|
||||||
|
export AIBOTK_NAME=""
|
||||||
|
|
||||||
|
## 14. SMTP
|
||||||
|
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
|
||||||
|
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||||
|
export SMTP_SERVER=""
|
||||||
|
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
|
||||||
|
export SMTP_SSL="false"
|
||||||
|
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
||||||
|
export SMTP_EMAIL=""
|
||||||
|
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
|
export SMTP_PASSWORD=""
|
||||||
|
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
|
||||||
|
export SMTP_NAME=""
|
||||||
|
|
||||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||||
|
|||||||
+244
-75
@@ -38,12 +38,19 @@ let SCKEY = '';
|
|||||||
//此处填你申请的PushDeer KEY.
|
//此处填你申请的PushDeer KEY.
|
||||||
//(环境变量名 DEER_KEY)
|
//(环境变量名 DEER_KEY)
|
||||||
let PUSHDEER_KEY = '';
|
let PUSHDEER_KEY = '';
|
||||||
|
let PUSHDEER_URL = '';
|
||||||
|
|
||||||
|
// =======================================Synology Chat通知设置区域===========================================
|
||||||
|
//此处填你申请的CHAT_URL与CHAT_TOKEN
|
||||||
|
//(环境变量名 CHAT_URL CHAT_TOKEN)
|
||||||
|
let CHAT_URL = '';
|
||||||
|
let CHAT_TOKEN = '';
|
||||||
|
|
||||||
// =======================================Bark App通知设置区域===========================================
|
// =======================================Bark App通知设置区域===========================================
|
||||||
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
|
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
|
||||||
let BARK_PUSH = '';
|
let BARK_PUSH = '';
|
||||||
//BARK app推送图标,自定义推送图标(需iOS15或以上)
|
//BARK app推送图标,自定义推送图标(需iOS15或以上)
|
||||||
let BARK_ICON = 'http://qn.whyour.cn/logo.png';
|
let BARK_ICON = 'https://qn.whyour.cn/logo.png';
|
||||||
//BARK app推送铃声,铃声列表去APP查看复制填写
|
//BARK app推送铃声,铃声列表去APP查看复制填写
|
||||||
let BARK_SOUND = '';
|
let BARK_SOUND = '';
|
||||||
//BARK app推送消息的分组, 默认为"QingLong"
|
//BARK app推送消息的分组, 默认为"QingLong"
|
||||||
@@ -104,6 +111,32 @@ let PUSH_PLUS_USER = '';
|
|||||||
let QQ_SKEY = '';
|
let QQ_SKEY = '';
|
||||||
let QQ_MODE = '';
|
let QQ_MODE = '';
|
||||||
|
|
||||||
|
// =======================================智能微秘书设置区域=======================================
|
||||||
|
//官方文档:http://wechat.aibotk.com/docs/about
|
||||||
|
//AIBOTK_KEY: 填写智能微秘书个人中心的apikey
|
||||||
|
//AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
|
||||||
|
//AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
|
||||||
|
let AIBOTK_KEY = '';
|
||||||
|
let AIBOTK_TYPE = '';
|
||||||
|
let AIBOTK_NAME = '';
|
||||||
|
|
||||||
|
// =======================================飞书机器人设置区域=======================================
|
||||||
|
//官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
|
||||||
|
//FSKEY 飞书机器人的 FSKEY
|
||||||
|
let FSKEY = '';
|
||||||
|
|
||||||
|
// =======================================SMTP 邮件设置区域=======================================
|
||||||
|
// SMTP_SERVER: 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||||
|
// SMTP_SSL: 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
|
||||||
|
// SMTP_EMAIL: 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
||||||
|
// SMTP_PASSWORD: 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
|
// SMTP_NAME: 填写 SMTP 收发件人姓名,可随意填写
|
||||||
|
let SMTP_SERVER = '';
|
||||||
|
let SMTP_SSL = 'false';
|
||||||
|
let SMTP_EMAIL = '';
|
||||||
|
let SMTP_PASSWORD = '';
|
||||||
|
let SMTP_NAME = '';
|
||||||
|
|
||||||
//==========================云端环境变量的判断与接收=========================
|
//==========================云端环境变量的判断与接收=========================
|
||||||
if (process.env.GOTIFY_URL) {
|
if (process.env.GOTIFY_URL) {
|
||||||
GOTIFY_URL = process.env.GOTIFY_URL;
|
GOTIFY_URL = process.env.GOTIFY_URL;
|
||||||
@@ -131,6 +164,15 @@ if (process.env.PUSH_KEY) {
|
|||||||
|
|
||||||
if (process.env.DEER_KEY) {
|
if (process.env.DEER_KEY) {
|
||||||
PUSHDEER_KEY = process.env.DEER_KEY;
|
PUSHDEER_KEY = process.env.DEER_KEY;
|
||||||
|
PUSHDEER_URL = process.env.DEER_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CHAT_URL) {
|
||||||
|
CHAT_URL = process.env.CHAT_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CHAT_TOKEN) {
|
||||||
|
CHAT_TOKEN = process.env.CHAT_TOKEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.QQ_SKEY) {
|
if (process.env.QQ_SKEY) {
|
||||||
@@ -206,6 +248,36 @@ if (process.env.PUSH_PLUS_TOKEN) {
|
|||||||
if (process.env.PUSH_PLUS_USER) {
|
if (process.env.PUSH_PLUS_USER) {
|
||||||
PUSH_PLUS_USER = process.env.PUSH_PLUS_USER;
|
PUSH_PLUS_USER = process.env.PUSH_PLUS_USER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.AIBOTK_KEY) {
|
||||||
|
AIBOTK_KEY = process.env.AIBOTK_KEY;
|
||||||
|
}
|
||||||
|
if (process.env.AIBOTK_TYPE) {
|
||||||
|
AIBOTK_TYPE = process.env.AIBOTK_TYPE;
|
||||||
|
}
|
||||||
|
if (process.env.AIBOTK_NAME) {
|
||||||
|
AIBOTK_NAME = process.env.AIBOTK_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.FSKEY) {
|
||||||
|
FSKEY = process.env.FSKEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.SMTP_SERVER) {
|
||||||
|
SMTP_SERVER = process.env.SMTP_SERVER;
|
||||||
|
}
|
||||||
|
if (process.env.SMTP_SSL) {
|
||||||
|
SMTP_SSL = process.env.SMTP_SSL;
|
||||||
|
}
|
||||||
|
if (process.env.SMTP_EMAIL) {
|
||||||
|
SMTP_EMAIL = process.env.SMTP_EMAIL;
|
||||||
|
}
|
||||||
|
if (process.env.SMTP_PASSWORD) {
|
||||||
|
SMTP_PASSWORD = process.env.SMTP_PASSWORD;
|
||||||
|
}
|
||||||
|
if (process.env.SMTP_NAME) {
|
||||||
|
SMTP_NAME = process.env.SMTP_NAME;
|
||||||
|
}
|
||||||
//==========================云端环境变量的判断与接收=========================
|
//==========================云端环境变量的判断与接收=========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -239,6 +311,11 @@ async function sendNotify(
|
|||||||
iGotNotify(text, desp, params), //iGot
|
iGotNotify(text, desp, params), //iGot
|
||||||
gobotNotify(text, desp), //go-cqhttp
|
gobotNotify(text, desp), //go-cqhttp
|
||||||
gotifyNotify(text, desp), //gotify
|
gotifyNotify(text, desp), //gotify
|
||||||
|
ChatNotify(text, desp), //synolog chat
|
||||||
|
PushDeerNotify(text, desp), //PushDeer
|
||||||
|
aibotkNotify(text, desp), //智能微秘书
|
||||||
|
fsBotNotify(text, desp), //飞书机器人
|
||||||
|
smtpNotify(text, desp), //SMTP 邮件
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +356,7 @@ function gotifyNotify(text, desp) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function gobotNotify(text, desp, time = 2100) {
|
function gobotNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (GOBOT_URL) {
|
if (GOBOT_URL) {
|
||||||
const options = {
|
const options = {
|
||||||
@@ -290,7 +367,6 @@ function gobotNotify(text, desp, time = 2100) {
|
|||||||
},
|
},
|
||||||
timeout,
|
timeout,
|
||||||
};
|
};
|
||||||
setTimeout(() => {
|
|
||||||
$.post(options, (err, resp, data) => {
|
$.post(options, (err, resp, data) => {
|
||||||
try {
|
try {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -303,9 +379,7 @@ function gobotNotify(text, desp, time = 2100) {
|
|||||||
} else if (data.retcode === 100) {
|
} else if (data.retcode === 100) {
|
||||||
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
|
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`);
|
||||||
`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -314,14 +388,13 @@ function gobotNotify(text, desp, time = 2100) {
|
|||||||
resolve(data);
|
resolve(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, time);
|
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function serverNotify(text, desp, time = 2100) {
|
function serverNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (SCKEY) {
|
if (SCKEY) {
|
||||||
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
|
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
|
||||||
@@ -336,7 +409,6 @@ function serverNotify(text, desp, time = 2100) {
|
|||||||
},
|
},
|
||||||
timeout,
|
timeout,
|
||||||
};
|
};
|
||||||
setTimeout(() => {
|
|
||||||
$.post(options, (err, resp, data) => {
|
$.post(options, (err, resp, data) => {
|
||||||
try {
|
try {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -351,9 +423,7 @@ function serverNotify(text, desp, time = 2100) {
|
|||||||
// 一分钟内发送相同的内容会触发
|
// 一分钟内发送相同的内容会触发
|
||||||
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
|
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(`server酱发送通知消息异常\n${JSON.stringify(data)}`);
|
||||||
`server酱发送通知消息异常\n${JSON.stringify(data)}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -362,27 +432,25 @@ function serverNotify(text, desp, time = 2100) {
|
|||||||
resolve(data);
|
resolve(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, time);
|
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function PushDeerNotify(text, desp, time = 2100) {
|
function PushDeerNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (PUSHDEER_KEY) {
|
if (PUSHDEER_KEY) {
|
||||||
// PushDeer 建议对消息内容进行 urlencode
|
// PushDeer 建议对消息内容进行 urlencode
|
||||||
desp = encodeURI(desp);
|
desp = encodeURI(desp);
|
||||||
const options = {
|
const options = {
|
||||||
url: `https://api2.pushdeer.com/message/push`,
|
url: PUSHDEER_URL || `https://api2.pushdeer.com/message/push`,
|
||||||
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
|
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
timeout,
|
timeout,
|
||||||
};
|
};
|
||||||
setTimeout(() => {
|
|
||||||
$.post(options, (err, resp, data) => {
|
$.post(options, (err, resp, data) => {
|
||||||
try {
|
try {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -397,9 +465,7 @@ function PushDeerNotify(text, desp, time = 2100) {
|
|||||||
) {
|
) {
|
||||||
console.log('PushDeer发送通知消息成功🎉\n');
|
console.log('PushDeer发送通知消息成功🎉\n');
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(`PushDeer发送通知消息异常\n${JSON.stringify(data)}`);
|
||||||
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -408,80 +474,39 @@ function PushDeerNotify(text, desp, time = 2100) {
|
|||||||
resolve(data);
|
resolve(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, time);
|
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoolPush(text, desp) {
|
function ChatNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (QQ_SKEY) {
|
if (CHAT_URL && CHAT_TOKEN) {
|
||||||
let options = {
|
// 对消息内容进行 urlencode
|
||||||
url: `https://push.xuthus.cc/${QQ_MODE}/${QQ_SKEY}`,
|
desp = encodeURI(desp);
|
||||||
|
const options = {
|
||||||
|
url: `${CHAT_URL}${CHAT_TOKEN}`,
|
||||||
|
body: `payload={"text":"${text}\n${desp}"}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 已知敏感词
|
|
||||||
text = text.replace(/京豆/g, '豆豆');
|
|
||||||
desp = desp.replace(/京豆/g, '');
|
|
||||||
desp = desp.replace(/🐶/g, '');
|
|
||||||
desp = desp.replace(/红包/g, 'H包');
|
|
||||||
|
|
||||||
switch (QQ_MODE) {
|
|
||||||
case 'email':
|
|
||||||
options.json = {
|
|
||||||
t: text,
|
|
||||||
c: desp,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
options.body = `${text}\n\n${desp}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pushMode = function (t) {
|
|
||||||
switch (t) {
|
|
||||||
case 'send':
|
|
||||||
return '个人';
|
|
||||||
case 'group':
|
|
||||||
return 'QQ群';
|
|
||||||
case 'wx':
|
|
||||||
return '微信';
|
|
||||||
case 'ww':
|
|
||||||
return '企业微信';
|
|
||||||
case 'email':
|
|
||||||
return '邮件';
|
|
||||||
default:
|
|
||||||
return '未知方式';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$.post(options, (err, resp, data) => {
|
$.post(options, (err, resp, data) => {
|
||||||
try {
|
try {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(`发送${pushMode(QQ_MODE)}通知调用API失败!!\n`);
|
console.log('发送通知调用API失败!!\n');
|
||||||
console.log(err);
|
console.log(err);
|
||||||
} else {
|
} else {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
if (data.code === 200) {
|
if (data.success) {
|
||||||
console.log(`酷推发送${pushMode(QQ_MODE)}通知消息成功🎉\n`);
|
console.log('Chat发送通知消息成功🎉\n');
|
||||||
} else if (data.code === 400) {
|
|
||||||
console.log(
|
|
||||||
`QQ酷推(Cool Push)发送${pushMode(QQ_MODE)}推送失败:${
|
|
||||||
data.msg
|
|
||||||
}\n`,
|
|
||||||
);
|
|
||||||
} else if (data.code === 503) {
|
|
||||||
console.log(`QQ酷推出错,${data.message}:${data.data}\n`);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`酷推推送异常: ${JSON.stringify(data)}`);
|
console.log(`Chat发送通知消息异常\n${JSON.stringify(data)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$.logErr(e, resp);
|
$.logErr(e);
|
||||||
} finally {
|
} finally {
|
||||||
resolve(data);
|
resolve(data);
|
||||||
}
|
}
|
||||||
@@ -594,7 +619,7 @@ function ddBotNotify(text, desp) {
|
|||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
content: ` ${text}\n\n${desp}`,
|
content: `${text}\n\n${desp}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
@@ -661,7 +686,7 @@ function qywxBotNotify(text, desp) {
|
|||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
content: ` ${text}\n\n${desp}`,
|
content: `${text}\n\n${desp}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
@@ -928,6 +953,150 @@ function pushPlusNotify(text, desp) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function aibotkNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (AIBOTK_KEY && AIBOTK_TYPE && AIBOTK_NAME) {
|
||||||
|
let json = {};
|
||||||
|
let url = '';
|
||||||
|
switch (AIBOTK_TYPE) {
|
||||||
|
case 'room':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
|
||||||
|
json = {
|
||||||
|
apiKey: `${AIBOTK_KEY}`,
|
||||||
|
roomName: `${AIBOTK_NAME}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${text}\n${desp}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'contact':
|
||||||
|
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
|
||||||
|
json = {
|
||||||
|
apiKey: `${AIBOTK_KEY}`,
|
||||||
|
name: `${AIBOTK_NAME}`,
|
||||||
|
message: {
|
||||||
|
type: 1,
|
||||||
|
content: `【青龙快讯】\n\n${text}\n${desp}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const options = {
|
||||||
|
url: url,
|
||||||
|
json,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('智能微秘书发送通知消息失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.code === 0) {
|
||||||
|
console.log('智能微秘书发送通知消息成功🎉。\n');
|
||||||
|
} else {
|
||||||
|
console.log(`${data.error}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fsBotNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (FSKEY) {
|
||||||
|
const options = {
|
||||||
|
url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,
|
||||||
|
json: { msg_type: 'text', content: { text: `${text}\n\n${desp}` } },
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('发送通知调用API失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.StatusCode === 0) {
|
||||||
|
console.log('飞书发送通知消息成功🎉\n');
|
||||||
|
} else {
|
||||||
|
console.log(`${data.msg}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function smtpNotify(text, desp) {
|
||||||
|
if (![SMTP_SERVER, SMTP_EMAIL, SMTP_PASSWORD].every(Boolean)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const transporter = nodemailer.createTransport(
|
||||||
|
`${SMTP_SSL === 'true' ? 'smtps:' : 'smtp:'}//${SMTP_SERVER}`,
|
||||||
|
{
|
||||||
|
auth: {
|
||||||
|
user: SMTP_EMAIL,
|
||||||
|
pass: SMTP_PASSWORD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: addr,
|
||||||
|
to: addr,
|
||||||
|
subject: text,
|
||||||
|
text: desp,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!!info.messageId) {
|
||||||
|
console.log('SMTP发送通知消息成功🎉\n');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log('SMTP发送通知消息失败!!\n');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('SMTP发送通知消息出现错误!!\n');
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function smtpNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
|
||||||
|
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendNotify,
|
sendNotify,
|
||||||
BARK_PUSH,
|
BARK_PUSH,
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import re
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.header import Header
|
||||||
|
from email.utils import formataddr
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -61,6 +65,10 @@ push_config = {
|
|||||||
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
||||||
|
|
||||||
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
|
'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_TOKEN': '', # push+ 微信推送的用户令牌
|
||||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||||
@@ -78,6 +86,16 @@ push_config = {
|
|||||||
'TG_PROXY_AUTH': '', # tg 代理认证参数
|
'TG_PROXY_AUTH': '', # tg 代理认证参数
|
||||||
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
|
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
|
||||||
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
|
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
|
||||||
|
|
||||||
|
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
|
||||||
|
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
|
||||||
|
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
|
||||||
|
|
||||||
|
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||||
|
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
|
||||||
|
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
|
||||||
|
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
|
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
||||||
}
|
}
|
||||||
notify_function = []
|
notify_function = []
|
||||||
# fmt: on
|
# fmt: on
|
||||||
@@ -273,6 +291,9 @@ def pushdeer(title: str, content: str) -> None:
|
|||||||
print("PushDeer 服务启动")
|
print("PushDeer 服务启动")
|
||||||
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
|
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
|
||||||
url = 'https://api2.pushdeer.com/message/push'
|
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()
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if len(response.get("content").get("result")) > 0:
|
if len(response.get("content").get("result")) > 0:
|
||||||
@@ -281,6 +302,25 @@ def pushdeer(title: str, content: str) -> None:
|
|||||||
print("PushDeer 推送失败!错误信息:", response)
|
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:
|
def pushplus_bot(title: str, content: str) -> None:
|
||||||
"""
|
"""
|
||||||
通过 push+ 推送消息。
|
通过 push+ 推送消息。
|
||||||
@@ -499,6 +539,63 @@ def telegram_bot(title: str, content: str) -> None:
|
|||||||
print("tg 推送失败!")
|
print("tg 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def aibotk(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 智能微秘书 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("AIBOTK_KEY") or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_NAME"):
|
||||||
|
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("智能微秘书 服务启动")
|
||||||
|
|
||||||
|
if push_config.get("AIBOTK_TYPE") == 'room':
|
||||||
|
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
|
||||||
|
data = {
|
||||||
|
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||||
|
"roomName": push_config.get("AIBOTK_NAME"),
|
||||||
|
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
|
||||||
|
data = {
|
||||||
|
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||||
|
"name": push_config.get("AIBOTK_NAME"),
|
||||||
|
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
|
||||||
|
}
|
||||||
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
|
print(response)
|
||||||
|
if response["code"] == 0:
|
||||||
|
print("智能微秘书 推送成功!")
|
||||||
|
else:
|
||||||
|
print(f'智能微秘书 推送失败!{response["error"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def smtp(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 SMTP 邮件 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("SMTP_SERVER") or not push_config.get("SMTP_SSL") or not push_config.get("SMTP_EMAIL") or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_NAME"):
|
||||||
|
print("SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("SMTP 邮件 服务启动")
|
||||||
|
|
||||||
|
message = MIMEText(content, 'plain', 'utf-8')
|
||||||
|
message['From'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
|
||||||
|
message['To'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
|
||||||
|
message['Subject'] = Header(title, 'utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get("SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
|
||||||
|
smtp_server.login(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD"))
|
||||||
|
smtp_server.sendmail(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_EMAIL"), message.as_bytes())
|
||||||
|
smtp_server.close()
|
||||||
|
print("SMTP 邮件 推送成功!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f'SMTP 邮件 推送失败!{e}')
|
||||||
|
|
||||||
|
|
||||||
def one() -> str:
|
def one() -> str:
|
||||||
"""
|
"""
|
||||||
获取一条一言。
|
获取一条一言。
|
||||||
@@ -527,6 +624,8 @@ if push_config.get("PUSH_KEY"):
|
|||||||
notify_function.append(serverJ)
|
notify_function.append(serverJ)
|
||||||
if push_config.get("DEER_KEY"):
|
if push_config.get("DEER_KEY"):
|
||||||
notify_function.append(pushdeer)
|
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"):
|
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
notify_function.append(pushplus_bot)
|
notify_function.append(pushplus_bot)
|
||||||
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||||
@@ -537,6 +636,10 @@ if push_config.get("QYWX_KEY"):
|
|||||||
notify_function.append(wecom_bot)
|
notify_function.append(wecom_bot)
|
||||||
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
|
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
|
||||||
notify_function.append(telegram_bot)
|
notify_function.append(telegram_bot)
|
||||||
|
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
|
||||||
|
notify_function.append(aibotk)
|
||||||
|
if push_config.get("SMTP_SERVER") and push_config.get("SMTP_SSL") and push_config.get("SMTP_EMAIL") and push_config.get("SMTP_PASSWORD") and push_config.get("SMTP_NAME"):
|
||||||
|
notify_function.append(smtp)
|
||||||
|
|
||||||
|
|
||||||
def send(title: str, content: str) -> None:
|
def send(title: str, content: str) -> None:
|
||||||
|
|||||||
+2
-1
@@ -5,8 +5,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
"download": "^8.0.0",
|
"download": "^8.0.0",
|
||||||
"http-server": "^0.12.3",
|
|
||||||
"got": "^11.5.1",
|
"got": "^11.5.1",
|
||||||
|
"http-server": "^0.12.3",
|
||||||
|
"nodemailer": "^6.8.0",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"tough-cookie": "^4.0.0",
|
"tough-cookie": "^4.0.0",
|
||||||
|
|||||||
+2
-2
@@ -5,14 +5,14 @@ const envFound = dotenv.config();
|
|||||||
const accessKey = process.env.QINIU_AK;
|
const accessKey = process.env.QINIU_AK;
|
||||||
const secretKey = process.env.QINIU_SK;
|
const secretKey = process.env.QINIU_SK;
|
||||||
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
|
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
|
||||||
const key = 'version.ts';
|
const key = 'version.yaml';
|
||||||
const options = {
|
const options = {
|
||||||
scope: `${process.env.QINIU_SCOPE}:${key}`,
|
scope: `${process.env.QINIU_SCOPE}:${key}`,
|
||||||
};
|
};
|
||||||
const putPolicy = new qiniu.rs.PutPolicy(options);
|
const putPolicy = new qiniu.rs.PutPolicy(options);
|
||||||
const uploadToken = putPolicy.uploadToken(mac);
|
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 config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
|
||||||
const formUploader = new qiniu.form_up.FormUploader(config);
|
const formUploader = new qiniu.form_up.FormUploader(config);
|
||||||
const putExtra = new qiniu.form_up.PutExtra(
|
const putExtra = new qiniu.form_up.PutExtra(
|
||||||
|
|||||||
+10
-1
@@ -1,7 +1,16 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
get_token() {
|
get_token() {
|
||||||
|
if [[ -f $file_auth_token ]]; then
|
||||||
token=$(cat $file_auth_token | jq -r .value)
|
token=$(cat $file_auth_token | jq -r .value)
|
||||||
|
else
|
||||||
|
local token_command="ts-node-transpile-only ${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")
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
add_cron_api() {
|
add_cron_api() {
|
||||||
@@ -154,7 +163,7 @@ update_cron() {
|
|||||||
code=$(echo $api | jq -r .code)
|
code=$(echo $api | jq -r .code)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo $api | jq -r .message)
|
||||||
if [[ $code != 200 ]]; then
|
if [[ $code != 200 ]]; then
|
||||||
echo -e "\n## 更新任务状态失败(${message})\n" >> $dir_log/$log_path
|
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -35,11 +35,10 @@ fi
|
|||||||
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
|
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
|
||||||
|
|
||||||
cd $dir_data
|
cd $dir_data
|
||||||
cat requirements.txt | while read LREAD
|
cat requirements.txt | while read LREAD; do
|
||||||
do
|
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
|
||||||
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
|
|
||||||
pip3 --default-timeout=100 install ${LREAD}
|
pip3 --default-timeout=100 install ${LREAD}
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo -e "\npython3依赖安装成功...\n"
|
echo -e "\npython3依赖安装成功...\n"
|
||||||
|
|||||||
@@ -87,10 +87,18 @@ check_pm2() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start_public() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
echo -e "=====> 开始检测"
|
echo -e "=====> 开始检测"
|
||||||
npm i -g pnpm
|
npm i -g pnpm
|
||||||
pnpm add -g pm2
|
pnpm add -g pm2
|
||||||
|
patch_version
|
||||||
|
start_public
|
||||||
copy_dep
|
copy_dep
|
||||||
check_ql
|
check_ql
|
||||||
check_nginx
|
check_nginx
|
||||||
|
|||||||
+281
@@ -0,0 +1,281 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
## 导入通用变量与函数
|
||||||
|
dir_shell=$QL_DIR/shell
|
||||||
|
. $dir_shell/share.sh
|
||||||
|
. $dir_shell/api.sh
|
||||||
|
|
||||||
|
trap "single_hanle" 2 20 15 14
|
||||||
|
single_hanle() {
|
||||||
|
handle_task_after "$@"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "\n命令未添加 \"now\",随机延迟 $delay_second 秒后执行\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}' | 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
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_task_before() {
|
||||||
|
begin_time=$(format_time "$time_format" "$time")
|
||||||
|
begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
|
||||||
|
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
|
||||||
|
echo -e "## 开始执行... $begin_time\n"
|
||||||
|
|
||||||
|
[[ $is_macos -eq 0 ]] && check_server
|
||||||
|
|
||||||
|
if [[ -s $task_error_log_path ]]; then
|
||||||
|
eval cat $task_error_log_path $cmd
|
||||||
|
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
|
||||||
|
eval echo $cmd
|
||||||
|
fi
|
||||||
|
|
||||||
|
. $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))
|
||||||
|
|
||||||
|
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
||||||
|
|
||||||
|
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
}
|
||||||
|
|
||||||
|
## 正常运行单个脚本,$1:传入参数
|
||||||
|
run_normal() {
|
||||||
|
local file_param=$1
|
||||||
|
if [[ $# -eq 1 ]]; 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
|
||||||
|
|
||||||
|
local envs=$(eval echo "\$${env_param}")
|
||||||
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
|
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=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||||
|
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||||
|
|
||||||
|
local envs=$(eval echo "\$${env_param}")
|
||||||
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
|
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
||||||
|
|
||||||
|
cd $dir_scripts
|
||||||
|
local relative_path="${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
|
||||||
|
|
||||||
|
local envs=$(eval echo "\$${env_param}")
|
||||||
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
|
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=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||||
|
[[ ! -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 "$@"
|
||||||
+1
-1
@@ -13,7 +13,7 @@ git push
|
|||||||
echo -e "更新cdn文件"
|
echo -e "更新cdn文件"
|
||||||
ts-node sample/tool.ts
|
ts-node 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"
|
version="v$string"
|
||||||
echo -e "当前版本$version"
|
echo -e "当前版本$version"
|
||||||
|
|
||||||
|
|||||||
+80
-22
@@ -78,9 +78,17 @@ import_config() {
|
|||||||
else
|
else
|
||||||
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cpu_warn=${CpuWarn}
|
||||||
|
mem_warn=${MemoryWarn}
|
||||||
|
disk_warn=${DiskWarn}
|
||||||
}
|
}
|
||||||
|
|
||||||
set_proxy() {
|
set_proxy() {
|
||||||
|
local proxy="$1"
|
||||||
|
if [[ $proxy ]]; then
|
||||||
|
proxy_url="$proxy"
|
||||||
|
fi
|
||||||
if [[ $proxy_url ]]; then
|
if [[ $proxy_url ]]; then
|
||||||
export http_proxy="${proxy_url}"
|
export http_proxy="${proxy_url}"
|
||||||
export https_proxy="${proxy_url}"
|
export https_proxy="${proxy_url}"
|
||||||
@@ -245,15 +253,13 @@ fix_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
npm_install_sub() {
|
npm_install_sub() {
|
||||||
set_proxy
|
|
||||||
if [ $is_termux -eq 1 ]; then
|
if [ $is_termux -eq 1 ]; then
|
||||||
npm install --production --no-bin-links --registry=https://registry.npmmirror.com || npm install --production --no-bin-links
|
npm install --production --no-bin-links
|
||||||
elif ! type pnpm &>/dev/null; then
|
elif ! type pnpm &>/dev/null; then
|
||||||
npm install --production --registry=https://registry.npmmirror.com || npm install --production
|
npm install --production
|
||||||
else
|
else
|
||||||
pnpm install --loglevel error --production --registry=https://registry.npmmirror.com || pnpm install --production --loglevel error
|
pnpm install --loglevel error --production
|
||||||
fi
|
fi
|
||||||
unset_proxy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
npm_install_1() {
|
npm_install_1() {
|
||||||
@@ -301,13 +307,14 @@ update_depend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
git_clone_scripts() {
|
git_clone_scripts() {
|
||||||
local url=$1
|
local url="$1"
|
||||||
local dir=$2
|
local dir="$2"
|
||||||
local branch=$3
|
local branch="$3"
|
||||||
|
local proxy="$4"
|
||||||
[[ $branch ]] && local part_cmd="-b $branch "
|
[[ $branch ]] && local part_cmd="-b $branch "
|
||||||
echo -e "开始克隆仓库 $url 到 $dir\n"
|
echo -e "开始克隆仓库 $url 到 $dir\n"
|
||||||
|
|
||||||
set_proxy
|
set_proxy "$proxy"
|
||||||
git clone $part_cmd $url $dir
|
git clone $part_cmd $url $dir
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
unset_proxy
|
unset_proxy
|
||||||
@@ -317,15 +324,15 @@ git_pull_scripts() {
|
|||||||
local dir_current=$(pwd)
|
local dir_current=$(pwd)
|
||||||
local dir_work="$1"
|
local dir_work="$1"
|
||||||
local branch="$2"
|
local branch="$2"
|
||||||
|
local proxy="$3"
|
||||||
cd $dir_work
|
cd $dir_work
|
||||||
echo -e "开始更新仓库:$dir_work\n"
|
echo -e "开始更新仓库:$dir_work\n"
|
||||||
|
|
||||||
set_proxy
|
set_proxy "$proxy"
|
||||||
git fetch --all
|
git fetch --all
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
git pull &>/dev/null
|
git pull &>/dev/null
|
||||||
unset_proxy
|
unset_proxy
|
||||||
reset_branch "$branch"
|
|
||||||
|
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
@@ -336,23 +343,25 @@ reset_romote_url() {
|
|||||||
local url=$2
|
local url=$2
|
||||||
local branch="$3"
|
local branch="$3"
|
||||||
|
|
||||||
if [[ -d "$dir_work/.git" ]]; then
|
|
||||||
cd $dir_work
|
cd $dir_work
|
||||||
|
if [[ -d "$dir_work/.git" ]]; then
|
||||||
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
||||||
git remote set-url origin $url &>/dev/null
|
git remote set-url origin $url &>/dev/null
|
||||||
|
else
|
||||||
local part_cmd=""
|
git init
|
||||||
|
git remote add origin $url &>/dev/null
|
||||||
|
fi
|
||||||
reset_branch "$branch"
|
reset_branch "$branch"
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset_branch() {
|
reset_branch() {
|
||||||
local branch="$1"
|
local branch="$1"
|
||||||
|
local part_cmd=""
|
||||||
if [[ $branch ]]; then
|
if [[ $branch ]]; then
|
||||||
part_cmd="origin/${branch}"
|
part_cmd="origin/${branch}"
|
||||||
git checkout -B "$branch"
|
git checkout -B "$branch" &>/dev/null
|
||||||
git branch --set-upstream-to=$part_cmd $branch
|
git branch --set-upstream-to=$part_cmd $branch &>/dev/null
|
||||||
fi
|
fi
|
||||||
git reset --hard $part_cmd &>/dev/null
|
git reset --hard $part_cmd &>/dev/null
|
||||||
}
|
}
|
||||||
@@ -373,10 +382,6 @@ reload_pm2() {
|
|||||||
echo -e "启动定时任务服务\n"
|
echo -e "启动定时任务服务\n"
|
||||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
pm2 delete schedule --source-map-support --time &>/dev/null
|
||||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||||
|
|
||||||
echo -e "启动公开服务\n"
|
|
||||||
pm2 delete public --source-map-support --time &>/dev/null
|
|
||||||
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
diff_time() {
|
diff_time() {
|
||||||
@@ -425,10 +430,63 @@ format_timestamp() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patch_version() {
|
||||||
|
if [[ $PipMirror ]]; then
|
||||||
|
pip3 config set global.index-url $PipMirror
|
||||||
|
fi
|
||||||
|
if [[ $NpmMirror ]]; then
|
||||||
|
npm config set registry $NpmMirror
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 兼容pnpm@7
|
||||||
|
pnpm setup &>/dev/null
|
||||||
|
source ~/.bashrc
|
||||||
|
pnpm install -g &>/dev/null
|
||||||
|
|
||||||
|
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 ! type ts-node &>/dev/null; then
|
||||||
|
pnpm add -g ts-node typescript tslib
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config --global pull.rebase false
|
||||||
|
|
||||||
|
cp -f $dir_root/.env.example $dir_root/.env
|
||||||
|
|
||||||
|
if [[ -d "$dir_root/db" ]]; then
|
||||||
|
echo -e "检测到旧的db目录,拷贝到data目录...\n"
|
||||||
|
cp -rf $dir_root/config $dir_root/data
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$dir_root/scripts" ]]; then
|
||||||
|
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
|
||||||
|
cp -rf $dir_root/scripts $dir_root/data
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$dir_root/log" ]]; then
|
||||||
|
echo -e "检测到旧的log目录,拷贝到data目录...\n"
|
||||||
|
cp -rf $dir_root/log $dir_root/data
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$dir_root/config" ]]; then
|
||||||
|
echo -e "检测到旧的config目录,拷贝到data目录...\n"
|
||||||
|
cp -rf $dir_root/config $dir_root/data
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
init_env
|
init_env
|
||||||
detect_termux
|
detect_termux
|
||||||
detect_macos
|
detect_macos
|
||||||
define_cmd
|
define_cmd
|
||||||
fix_config
|
fix_config
|
||||||
|
|
||||||
import_config $1 >$task_error_log_path 2>&1
|
import_config $1 2>$task_error_log_path
|
||||||
|
|||||||
+31
-348
@@ -21,85 +21,18 @@ define_program() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
random_delay() {
|
handle_log_path() {
|
||||||
local random_delay_max=$RandomDelay
|
|
||||||
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
|
|
||||||
local file_param=$1
|
local file_param=$1
|
||||||
local file_extensions=${RandomDelayFileExtensions-"js"}
|
|
||||||
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
|
|
||||||
|
|
||||||
if [[ -n $file_extensions ]]; then
|
if [[ -z $file_param ]];then
|
||||||
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
|
file_param="task"
|
||||||
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local current_min
|
local suffix=""
|
||||||
current_min=$(date "+%-M")
|
if [[ ! -z $ID ]]; then
|
||||||
for minute in $ignored_minutes; do
|
suffix="_${ID}"
|
||||||
if [[ $current_min -eq $minute ]]; then
|
|
||||||
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
|
|
||||||
return
|
|
||||||
fi
|
fi
|
||||||
done
|
time=$(date "+$time_format")
|
||||||
|
|
||||||
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
|
|
||||||
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\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}' | head -1)
|
|
||||||
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
cd $dir_current
|
|
||||||
}
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
usage() {
|
|
||||||
define_cmd
|
|
||||||
gen_array_scripts
|
|
||||||
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
|
|
||||||
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
|
|
||||||
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
|
|
||||||
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
|
|
||||||
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
|
|
||||||
if [[ ${#array_scripts[*]} -gt 0 ]]; then
|
|
||||||
echo -e "\n当前有以下脚本可以运行:"
|
|
||||||
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
|
|
||||||
echo -e "$(($i + 1)). ${array_scripts_name[i]}:${array_scripts[i]}"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo -e "\n暂无脚本可以执行"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
## run nohup,$1:文件名,不含路径,带后缀
|
|
||||||
run_nohup() {
|
|
||||||
local file_name=$1
|
|
||||||
nohup node $file_name &>$log_path &
|
|
||||||
}
|
|
||||||
|
|
||||||
## 正常运行单个脚本,$1:传入参数
|
|
||||||
run_normal() {
|
|
||||||
local file_param=$1
|
|
||||||
define_program "$file_param"
|
|
||||||
if [[ $# -eq 1 ]]; then
|
|
||||||
random_delay "$file_param"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local time=$(date "+$time_format")
|
|
||||||
log_time=$(format_log_time "$time_format" "$time")
|
log_time=$(format_log_time "$time_format" "$time")
|
||||||
log_dir_tmp="${file_param##*/}"
|
log_dir_tmp="${file_param##*/}"
|
||||||
if [[ $file_param =~ "/" ]]; then
|
if [[ $file_param =~ "/" ]]; then
|
||||||
@@ -112,296 +45,46 @@ run_normal() {
|
|||||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
log_dir="${log_dir_tmp%.*}"
|
log_dir="${log_dir_tmp%.*}${suffix}"
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
cmd=">> $dir_log/$log_path 2>&1"
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
[[ "$show_log" == "true" ]] && cmd=""
|
if [[ "$show_log" == "true" ]]; then
|
||||||
make_dir "$dir_log/$log_dir"
|
cmd=""
|
||||||
|
|
||||||
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
|
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"
|
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
|
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() {
|
format_params() {
|
||||||
local file_param="$1"
|
time_format="%Y-%m-%d %H:%M:%S"
|
||||||
local env_param="$2"
|
timeoutCmd=""
|
||||||
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
if type timeout &>/dev/null; then
|
||||||
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
|
timeoutCmd="timeout --foreground -s 14 -k 10s $command_timeout_time "
|
||||||
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## 运行其他命令
|
while getopts ":lm:" opt; do
|
||||||
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
|
case $opt in
|
||||||
l)
|
l)
|
||||||
show_log="true"
|
show_log="true"
|
||||||
;;
|
;;
|
||||||
esac
|
m)
|
||||||
done
|
max_time="$OPTARG"
|
||||||
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
|
||||||
|
|
||||||
timeoutCmd=""
|
|
||||||
if type timeout &>/dev/null; then
|
|
||||||
timeoutCmd="timeout -k 10s $command_timeout_time "
|
|
||||||
fi
|
|
||||||
|
|
||||||
time_format="%Y-%m-%d %H:%M:%S"
|
|
||||||
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.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
|
||||||
;;
|
done
|
||||||
esac
|
[[ $show_log ]] && shift $(($OPTIND - 1))
|
||||||
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
|
if [[ $max_time ]]; then
|
||||||
elif [[ $# -eq 0 ]]; then
|
shift $(($OPTIND - 1))
|
||||||
echo
|
command_timeout_time="$max_time"
|
||||||
usage
|
fi
|
||||||
else
|
|
||||||
run_else "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
format_params "$@"
|
||||||
|
define_program "$@"
|
||||||
|
handle_log_path "$@"
|
||||||
|
|
||||||
|
eval . $dir_shell/otask.sh "$cmd"
|
||||||
|
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+114
-141
@@ -26,33 +26,6 @@ diff_cron() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
## 检测配置文件版本
|
|
||||||
detect_config_version() {
|
|
||||||
## 识别出两个文件的版本号
|
|
||||||
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
|
||||||
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
|
||||||
|
|
||||||
## 删除旧的发送记录文件
|
|
||||||
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
|
|
||||||
|
|
||||||
## 识别出更新日期和更新内容
|
|
||||||
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
|
|
||||||
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
|
|
||||||
|
|
||||||
## 如果是今天,并且版本号不一致,则发送通知
|
|
||||||
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
|
|
||||||
if [[ ! -f $send_mark ]]; then
|
|
||||||
local notify_title="配置文件更新通知"
|
|
||||||
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
|
|
||||||
echo -e $notify_content
|
|
||||||
notify_api "$notify_title" "$notify_content"
|
|
||||||
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
[[ -f $send_mark ]] && rm -f $send_mark
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
|
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
|
||||||
output_list_add_drop() {
|
output_list_add_drop() {
|
||||||
local list=$1
|
local list=$1
|
||||||
@@ -144,6 +117,9 @@ update_repo() {
|
|||||||
local dependence="$4"
|
local dependence="$4"
|
||||||
local branch="$5"
|
local branch="$5"
|
||||||
local extensions="$6"
|
local extensions="$6"
|
||||||
|
local proxy="$7"
|
||||||
|
local autoAddCron="$8"
|
||||||
|
local autoDelCron="$9"
|
||||||
local tmp="${url%/*}"
|
local tmp="${url%/*}"
|
||||||
local authorTmp1="${tmp##*/}"
|
local authorTmp1="${tmp##*/}"
|
||||||
local authorTmp2="${authorTmp1##*:}"
|
local authorTmp2="${authorTmp1##*:}"
|
||||||
@@ -156,13 +132,14 @@ update_repo() {
|
|||||||
local formatUrl="$url"
|
local formatUrl="$url"
|
||||||
if [[ -d ${repo_path}/.git ]]; then
|
if [[ -d ${repo_path}/.git ]]; then
|
||||||
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
|
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
|
||||||
git_pull_scripts ${repo_path} "${branch}"
|
git_pull_scripts ${repo_path} "${branch}" "${proxy}"
|
||||||
else
|
else
|
||||||
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
|
rm -rf ${repo_path} &>/dev/null
|
||||||
|
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
|
||||||
fi
|
fi
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新${repo_path}成功...\n"
|
echo -e "\n更新${repo_path}成功...\n"
|
||||||
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
|
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
|
||||||
else
|
else
|
||||||
echo -e "\n更新${repo_path}失败,请检查网络...\n"
|
echo -e "\n更新${repo_path}失败,请检查网络...\n"
|
||||||
fi
|
fi
|
||||||
@@ -186,7 +163,7 @@ update_raw() {
|
|||||||
echo -e "下载 ${raw_file_name} 成功...\n"
|
echo -e "下载 ${raw_file_name} 成功...\n"
|
||||||
cd $dir_raw
|
cd $dir_raw
|
||||||
local filename="raw_${raw_file_name}"
|
local filename="raw_${raw_file_name}"
|
||||||
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
|
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
|
||||||
cp -f $raw_file_name $dir_scripts/${filename}
|
cp -f $raw_file_name $dir_scripts/${filename}
|
||||||
cron_line=$(
|
cron_line=$(
|
||||||
perl -ne "{
|
perl -ne "{
|
||||||
@@ -230,7 +207,7 @@ run_extra_shell() {
|
|||||||
|
|
||||||
## 脚本用法
|
## 脚本用法
|
||||||
usage() {
|
usage() {
|
||||||
echo -e "本脚本用法:"
|
echo -e "ql命令使用方法:"
|
||||||
echo -e "1. $cmd_update update # 更新并重启青龙"
|
echo -e "1. $cmd_update update # 更新并重启青龙"
|
||||||
echo -e "2. $cmd_update extra # 运行自定义脚本"
|
echo -e "2. $cmd_update extra # 运行自定义脚本"
|
||||||
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
|
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
|
||||||
@@ -244,41 +221,52 @@ usage() {
|
|||||||
|
|
||||||
## 更新qinglong
|
## 更新qinglong
|
||||||
update_qinglong() {
|
update_qinglong() {
|
||||||
patch_version
|
local mirror="github"
|
||||||
|
local githubStatus=$(curl -s -m 2 -IL "https://github.com" | grep 200)
|
||||||
|
if [ "$githubStatus" == "" ]; then
|
||||||
|
mirror="gitee"
|
||||||
|
fi
|
||||||
|
echo -e "使用 ${mirror} 源更新...\n"
|
||||||
|
export isFirstStartServer=false
|
||||||
|
|
||||||
local no_restart="$1"
|
local all_branch=$(cd ${dir_root} && git branch -a)
|
||||||
local all_branch=$(git branch -a)
|
|
||||||
local primary_branch="master"
|
local primary_branch="master"
|
||||||
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
|
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
|
||||||
primary_branch="${current_branch}"
|
primary_branch="${current_branch}"
|
||||||
fi
|
fi
|
||||||
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
|
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
|
||||||
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
|
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
|
||||||
git_pull_scripts $dir_root ${primary_branch}
|
git_pull_scripts $dir_root ${primary_branch}
|
||||||
|
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新青龙源文件成功...\n"
|
echo -e "\n更新青龙源文件成功...\n"
|
||||||
|
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
|
||||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||||
detect_config_version
|
|
||||||
update_depend
|
update_depend
|
||||||
|
|
||||||
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
||||||
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
||||||
else
|
|
||||||
echo -e "\n更新青龙源文件失败,请检查原因...\n"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local url="https://github.com/whyour/qinglong-static.git"
|
update_qinglong_static "$1" "$primary_branch"
|
||||||
|
else
|
||||||
|
echo -e "\n更新青龙源文件失败,请检查网络...\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update_qinglong_static() {
|
||||||
|
local no_restart="$1"
|
||||||
|
local primary_branch="$2"
|
||||||
|
local url="https://${mirror}.com/whyour/qinglong-static.git"
|
||||||
if [[ -d ${ql_static_repo}/.git ]]; then
|
if [[ -d ${ql_static_repo}/.git ]]; then
|
||||||
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
|
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
|
||||||
git_pull_scripts ${ql_static_repo} ${primary_branch}
|
git_pull_scripts ${ql_static_repo} ${primary_branch}
|
||||||
else
|
else
|
||||||
|
rm -rf ${ql_static_repo} &>/dev/null
|
||||||
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
|
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
|
||||||
fi
|
fi
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新青龙静态资源成功...\n"
|
echo -e "\n更新青龙静态资源成功...\n"
|
||||||
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
|
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
|
||||||
echo -e "\n当前版本 $static_version...\n"
|
|
||||||
|
|
||||||
rm -rf $dir_static/*
|
rm -rf $dir_static/*
|
||||||
cp -rf $ql_static_repo/* $dir_static
|
cp -rf $ql_static_repo/* $dir_static
|
||||||
@@ -289,55 +277,8 @@ update_qinglong() {
|
|||||||
reload_pm2
|
reload_pm2
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "\n更新青龙静态资源失败,请检查原因...\n"
|
echo -e "\n更新青龙静态资源失败,请检查网络...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
patch_version() {
|
|
||||||
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 ! type ts-node &>/dev/null; then
|
|
||||||
pnpm i -g ts-node typescript tslib
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 兼容pnpm@7
|
|
||||||
pnpm setup
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
git config --global pull.rebase false
|
|
||||||
|
|
||||||
cp -f $dir_root/.env.example $dir_root/.env
|
|
||||||
|
|
||||||
if [[ -d "$dir_root/db" ]]; then
|
|
||||||
echo -e "检测到旧的db目录,拷贝到data目录...\n"
|
|
||||||
cp -rf $dir_root/config $dir_root/data
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$dir_root/scripts" ]]; then
|
|
||||||
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
|
|
||||||
cp -rf $dir_root/scripts $dir_root/data
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$dir_root/log" ]]; then
|
|
||||||
echo -e "检测到旧的log目录,拷贝到data目录...\n"
|
|
||||||
cp -rf $dir_root/log $dir_root/data
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$dir_root/config" ]]; then
|
|
||||||
echo -e "检测到旧的config目录,拷贝到data目录...\n"
|
|
||||||
cp -rf $dir_root/config $dir_root/data
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## 对比脚本
|
## 对比脚本
|
||||||
@@ -349,6 +290,15 @@ diff_scripts() {
|
|||||||
local blackword="$4"
|
local blackword="$4"
|
||||||
local dependence="$5"
|
local dependence="$5"
|
||||||
local extensions="$6"
|
local extensions="$6"
|
||||||
|
local autoAddCron="$7"
|
||||||
|
local autoDelCron="$8"
|
||||||
|
|
||||||
|
if [[ ! $autoAddCron ]];then
|
||||||
|
autoAddCron=${AutoAddCron}
|
||||||
|
fi
|
||||||
|
if [[ ! $autoDelCron ]];then
|
||||||
|
autoDelCron=${AutoDelCron}
|
||||||
|
fi
|
||||||
|
|
||||||
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
|
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
|
||||||
|
|
||||||
@@ -358,13 +308,13 @@ diff_scripts() {
|
|||||||
|
|
||||||
if [[ -s $list_drop ]]; then
|
if [[ -s $list_drop ]]; then
|
||||||
output_list_add_drop $list_drop "失效"
|
output_list_add_drop $list_drop "失效"
|
||||||
if [[ ${AutoDelCron} == true ]]; then
|
if [[ ${autoDelCron} == true ]]; then
|
||||||
del_cron $list_drop $uniq_path
|
del_cron $list_drop $uniq_path
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ -s $list_add ]]; then
|
if [[ -s $list_add ]]; then
|
||||||
output_list_add_drop $list_add "新"
|
output_list_add_drop $list_add "新"
|
||||||
if [[ ${AutoAddCron} == true ]]; then
|
if [[ ${autoAddCron} == true ]]; then
|
||||||
add_cron $list_add $uniq_path
|
add_cron $list_add $uniq_path
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -429,12 +379,12 @@ gen_list_repo() {
|
|||||||
filename=$(basename $file)
|
filename=$(basename $file)
|
||||||
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
|
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
|
||||||
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
|
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
|
||||||
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
# cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
if [[ $cron_id ]]; then
|
# if [[ $cron_id ]]; then
|
||||||
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
|
# result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
|
||||||
fi
|
# fi
|
||||||
done
|
done
|
||||||
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
|
grep -E "${cmd_task}.* ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task}.* (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,8 +406,7 @@ get_uniq_path() {
|
|||||||
main() {
|
main() {
|
||||||
## for ql update
|
## for ql update
|
||||||
show_log="false"
|
show_log="false"
|
||||||
while getopts ":l" opt
|
while getopts ":l" opt; do
|
||||||
do
|
|
||||||
case $opt in
|
case $opt in
|
||||||
l)
|
l)
|
||||||
show_log="true"
|
show_log="true"
|
||||||
@@ -466,37 +415,61 @@ main() {
|
|||||||
done
|
done
|
||||||
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
||||||
|
|
||||||
local p1=$1
|
local p1="${1}"
|
||||||
local p2=$2
|
local p2="${2}"
|
||||||
local p3=$3
|
local p3="${3}"
|
||||||
local p4=$4
|
local p4="${4}"
|
||||||
local p5=$5
|
local p5="${5}"
|
||||||
local p6=$6
|
local p6="${6}"
|
||||||
local p7=$7
|
local p7="${7}"
|
||||||
|
local p8="${8}"
|
||||||
|
local p9="${9}"
|
||||||
|
local p10="${10}"
|
||||||
|
local log_dir="${p1}"
|
||||||
|
make_dir "$dir_log/$log_dir"
|
||||||
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||||
local log_path="$dir_log/update/${log_time}_$p1.log"
|
local log_path="${log_dir}/${log_time}.log"
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local file_path="$dir_log/$log_path"
|
||||||
|
|
||||||
|
cmd=">> $file_path 2>&1"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
|
|
||||||
|
local time_format="%Y-%m-%d %H:%M:%S"
|
||||||
|
local time=$(date "+$time_format")
|
||||||
|
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
|
||||||
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
|
|
||||||
|
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
|
||||||
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -s $task_error_log_path ]]; then
|
||||||
|
eval cat $task_error_log_path $cmd
|
||||||
|
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
|
||||||
|
eval echo $cmd
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
|
||||||
|
eval echo -e "请移除 -l 参数" $cmd
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
case $p1 in
|
case $p1 in
|
||||||
update)
|
update)
|
||||||
cmd=">> $log_path 2>&1"
|
|
||||||
[[ "$show_log" == "true" ]] && cmd=""
|
|
||||||
eval echo -e "## 开始执行... $begin_time\n" $cmd
|
|
||||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
|
||||||
eval update_qinglong "$2" $cmd
|
eval update_qinglong "$2" $cmd
|
||||||
;;
|
;;
|
||||||
extra)
|
extra)
|
||||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
eval run_extra_shell $cmd
|
||||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
|
||||||
run_extra_shell >>$log_path
|
|
||||||
;;
|
;;
|
||||||
repo)
|
repo)
|
||||||
get_uniq_path "$p2" "$p6"
|
get_uniq_path "$p2" "$p6"
|
||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7"
|
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" "$p9" "$p10"
|
||||||
else
|
else
|
||||||
echo -e "命令输入错误...\n"
|
eval echo -e "命令输入错误...\\\n" $cmd
|
||||||
usage
|
eval usage $cmd
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
raw)
|
raw)
|
||||||
@@ -504,47 +477,47 @@ main() {
|
|||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_raw "$p2"
|
update_raw "$p2"
|
||||||
else
|
else
|
||||||
echo -e "命令输入错误...\n"
|
eval echo -e "命令输入错误...\\\n" $cmd
|
||||||
usage
|
eval usage $cmd
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
rmlog)
|
rmlog)
|
||||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
eval . $dir_shell/rmlog.sh "$p2" $cmd
|
||||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
|
||||||
. $dir_shell/rmlog.sh "$p2" >>$log_path
|
|
||||||
;;
|
;;
|
||||||
bot)
|
bot)
|
||||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
eval . $dir_shell/bot.sh $cmd
|
||||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
|
||||||
. $dir_shell/bot.sh >>$log_path
|
|
||||||
;;
|
;;
|
||||||
check)
|
check)
|
||||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
eval . $dir_shell/check.sh $cmd
|
||||||
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
|
||||||
. $dir_shell/check.sh >>$log_path
|
|
||||||
;;
|
;;
|
||||||
resetlet)
|
resetlet)
|
||||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
|
||||||
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
|
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
|
||||||
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
|
|
||||||
echo "$auth_value" >$file_auth_user
|
echo "$auth_value" >$file_auth_user
|
||||||
|
eval echo -e "重置登录错误次数成功" $cmd
|
||||||
;;
|
;;
|
||||||
resettfa)
|
resettfa)
|
||||||
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
|
||||||
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
|
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
|
||||||
echo -e "禁用两步验证成功 \n $auth_value" >>$log_path
|
|
||||||
echo "$auth_value" >$file_auth_user
|
echo "$auth_value" >$file_auth_user
|
||||||
|
eval echo -e "禁用两步验证成功" $cmd
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e "命令输入错误...\n"
|
eval echo -e "命令输入错误...\\\n" $cmd
|
||||||
usage
|
eval usage $cmd
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
local diff_time=$(diff_time "%Y-%m-%d %H:%M:%S" "$begin_time" "$end_time")
|
local etime=$(date "+$time_format")
|
||||||
if [[ $p1 != "repo" ]] && [[ $p1 != "raw" ]]; then
|
local end_time=$(format_time "$time_format" "$etime")
|
||||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒" >>$log_path
|
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||||
cat $log_path
|
local diff_time=$(($end_timestamp - $begin_timestamp))
|
||||||
|
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
|
||||||
|
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
|
||||||
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒 " $cmd
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f $file_path ]]; then
|
||||||
|
cat $file_path
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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 ? '复制成功' : '复制'}>
|
||||||
|
{copied ? <CheckOutlined /> : <CopyOutlined />}
|
||||||
|
</Tooltip>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Copy;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createFromIconfontCN } from '@ant-design/icons';
|
import { createFromIconfontCN } from '@ant-design/icons';
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
const IconFont = createFromIconfontCN({
|
||||||
scriptUrl: ['//at.alicdn.com/t/font_3354854_ds8pa06q1qa.js'],
|
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default IconFont;
|
export default IconFont;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const EditableTagGroup = ({
|
|||||||
{tag}
|
{tag}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
|
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
|
||||||
{tagElem}
|
{tagElem}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
treeData: any[],
|
||||||
|
searchValue: string,
|
||||||
|
{
|
||||||
|
treeNodeFilterProp,
|
||||||
|
}: {
|
||||||
|
treeNodeFilterProp: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
return useMemo(() => {
|
||||||
|
const keys: string[] = [];
|
||||||
|
|
||||||
|
if (!searchValue) {
|
||||||
|
return { treeData, keys };
|
||||||
|
}
|
||||||
|
|
||||||
|
const upperStr = searchValue.toUpperCase();
|
||||||
|
function filterOptionFunc(_: string, dataNode: any[]) {
|
||||||
|
const value = dataNode[treeNodeFilterProp as any];
|
||||||
|
|
||||||
|
return String(value).toUpperCase().includes(upperStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dig(list: any[], keepAll: boolean = false): any[] {
|
||||||
|
return list
|
||||||
|
.map((dataNode) => {
|
||||||
|
const children = dataNode.children;
|
||||||
|
|
||||||
|
const match = keepAll || filterOptionFunc!(searchValue, dataNode);
|
||||||
|
const childList = dig(children || [], match);
|
||||||
|
|
||||||
|
if (match || childList.length) {
|
||||||
|
childList.length && keys.push(dataNode.key);
|
||||||
|
return {
|
||||||
|
...dataNode,
|
||||||
|
children: childList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((node) => node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { treeData: dig(treeData), keys };
|
||||||
|
}, [treeData, searchValue, treeNodeFilterProp]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { MutableRefObject, useLayoutEffect, useState } from 'react';
|
||||||
|
import useResizeObserver from '@react-hook/resize-observer'
|
||||||
|
import { getTableScroll } from '@/utils';
|
||||||
|
|
||||||
|
export default <T extends HTMLElement>(target: MutableRefObject<T>, extraHeight?: number) => {
|
||||||
|
const [height, setHeight] = useState<number>()
|
||||||
|
|
||||||
|
useResizeObserver(target, (entry) => {
|
||||||
|
let _targe = entry.target as any
|
||||||
|
if (!_targe.classList.contains('ant-table-wrapper')) {
|
||||||
|
_targe = entry.target.querySelector('.ant-table-wrapper')
|
||||||
|
}
|
||||||
|
setHeight(getTableScroll({ extraHeight, target: _targe as HTMLElement }))
|
||||||
|
})
|
||||||
|
return height
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ContainerOutlined,
|
ContainerOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import IconFont from '@/components/iconfont';
|
import IconFont from '@/components/iconfont';
|
||||||
|
import { BasicLayoutProps } from '@ant-design/pro-layout';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
route: {
|
route: {
|
||||||
@@ -68,18 +69,18 @@ export default {
|
|||||||
icon: <IconFont type="ql-icon-dependence" />,
|
icon: <IconFont type="ql-icon-dependence" />,
|
||||||
component: '@/pages/dependence/index',
|
component: '@/pages/dependence/index',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/log',
|
||||||
|
name: '日志管理',
|
||||||
|
icon: <IconFont type="ql-icon-log" />,
|
||||||
|
component: '@/pages/log/index',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/diff',
|
path: '/diff',
|
||||||
name: '对比工具',
|
name: '对比工具',
|
||||||
icon: <IconFont type="ql-icon-diff" />,
|
icon: <IconFont type="ql-icon-diff" />,
|
||||||
component: '@/pages/diff/index',
|
component: '@/pages/diff/index',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/log',
|
|
||||||
name: '任务日志',
|
|
||||||
icon: <IconFont type="ql-icon-log" />,
|
|
||||||
component: '@/pages/log/index',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/setting',
|
path: '/setting',
|
||||||
name: '系统设置',
|
name: '系统设置',
|
||||||
@@ -93,4 +94,4 @@ export default {
|
|||||||
contentWidth: 'Fixed',
|
contentWidth: 'Fixed',
|
||||||
splitMenus: false,
|
splitMenus: false,
|
||||||
siderWidth: 180,
|
siderWidth: 180,
|
||||||
} as any;
|
} as BasicLayoutProps;
|
||||||
|
|||||||
+31
-27
@@ -10,8 +10,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-body {
|
.ant-modal-body {
|
||||||
max-height: calc(85vh - 110px);
|
max-height: calc(80vh - 110px);
|
||||||
max-height: calc(85vh - var(--vh-offset, 110px));
|
max-height: calc(80vh - var(--vh-offset, 110px));
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
&.env-wrapper,
|
&.env-wrapper,
|
||||||
&.config-wrapper {
|
&.config-wrapper {
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
width: calc(100vw - 80px);
|
width: calc(100vw - 24px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,8 +179,8 @@
|
|||||||
|
|
||||||
.Resizer {
|
.Resizer {
|
||||||
background: @component-background;
|
background: @component-background;
|
||||||
opacity: 0.2;
|
opacity: 0.8;
|
||||||
z-index: 1;
|
z-index: 100;
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -243,31 +243,29 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-dark='true'] {
|
/* Change autocomplete styles in WebKit */
|
||||||
/* Change autocomplete styles in WebKit */
|
input:-webkit-autofill,
|
||||||
input:-webkit-autofill,
|
input:-webkit-autofill:hover,
|
||||||
input:-webkit-autofill:hover,
|
input:-webkit-autofill:focus,
|
||||||
input:-webkit-autofill:focus,
|
textarea:-webkit-autofill,
|
||||||
textarea:-webkit-autofill,
|
textarea:-webkit-autofill:hover,
|
||||||
textarea:-webkit-autofill:hover,
|
textarea:-webkit-autofill:focus,
|
||||||
textarea:-webkit-autofill:focus,
|
select:-webkit-autofill,
|
||||||
select:-webkit-autofill,
|
select:-webkit-autofill:hover,
|
||||||
select:-webkit-autofill:hover,
|
select:-webkit-autofill:focus {
|
||||||
select:-webkit-autofill:focus {
|
|
||||||
border: 1px solid @border-color-base;
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: background-color 5000s ease-in-out 0s;
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
-webkit-text-fill-color: @text-color;
|
-webkit-text-fill-color: @text-color;
|
||||||
caret-color: #e8e6f3 !important;
|
caret-color: @text-color;
|
||||||
}
|
color: @text-color;
|
||||||
|
}
|
||||||
|
|
||||||
::placeholder {
|
::placeholder {
|
||||||
opacity: 0.5 !important;
|
opacity: 0.5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-select-selection-placeholder {
|
.ant-select-selection-placeholder {
|
||||||
opacity: 0.5 !important;
|
opacity: 0.5 !important;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-basicLayout-content {
|
.ant-pro-basicLayout-content {
|
||||||
@@ -326,10 +324,6 @@
|
|||||||
background-color: #373739;
|
background-color: #373739;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-layout-sider {
|
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.06) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-pro-sider-logo {
|
.ant-pro-sider-logo {
|
||||||
padding: 16px 8px !important;
|
padding: 16px 8px !important;
|
||||||
|
|
||||||
@@ -349,3 +343,13 @@
|
|||||||
width: auto !important;
|
width: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
word-break: break-all !important;
|
||||||
|
white-space: break-spaces !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtuallist .ant-table-tbody > tr > td > div {
|
||||||
|
white-space: unset !important;
|
||||||
|
}
|
||||||
|
|||||||
+98
-53
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
|
|||||||
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
|
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
|
||||||
import * as DarkReader from '@umijs/ssr-darkreader';
|
import * as DarkReader from '@umijs/ssr-darkreader';
|
||||||
import defaultProps from './defaultProps';
|
import defaultProps from './defaultProps';
|
||||||
import { Link, history } from 'umi';
|
import { Link, history, Outlet, useLocation } from '@umijs/max';
|
||||||
import {
|
import {
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
@@ -13,23 +13,60 @@ import config from '@/utils/config';
|
|||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import vhCheck from 'vh-check';
|
import vhCheck from 'vh-check';
|
||||||
import { version, changeLogLink, changeLog } from '../version';
|
|
||||||
import { useCtx, useTheme } from '@/utils/hooks';
|
import { useCtx, useTheme } from '@/utils/hooks';
|
||||||
import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
|
import {
|
||||||
|
message,
|
||||||
|
Badge,
|
||||||
|
Modal,
|
||||||
|
Avatar,
|
||||||
|
Dropdown,
|
||||||
|
Menu,
|
||||||
|
Image,
|
||||||
|
Popover,
|
||||||
|
Descriptions,
|
||||||
|
Tooltip,
|
||||||
|
MenuProps,
|
||||||
|
} from 'antd';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import SockJS from 'sockjs-client';
|
import SockJS from 'sockjs-client';
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { init } from '../utils/init';
|
import { init } from '../utils/init';
|
||||||
|
import 'codemirror/mode/javascript/javascript';
|
||||||
|
import 'codemirror/mode/python/python';
|
||||||
|
import 'codemirror/mode/shell/shell';
|
||||||
|
|
||||||
export default function (props: any) {
|
export interface SharedContext {
|
||||||
|
headerStyle: React.CSSProperties;
|
||||||
|
isPhone: boolean;
|
||||||
|
theme: 'vs' | 'vs-dark';
|
||||||
|
user: any;
|
||||||
|
reloadUser: (needLoading?: boolean) => void;
|
||||||
|
reloadTheme: () => void;
|
||||||
|
socketMessage: any;
|
||||||
|
systemInfo: TSystemInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TSystemInfo {
|
||||||
|
branch: 'develop' | 'master';
|
||||||
|
isInitialized: boolean;
|
||||||
|
lastCommitId: string;
|
||||||
|
lastCommitTime: number;
|
||||||
|
version: string;
|
||||||
|
changeLog: string;
|
||||||
|
changeLogLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const location = useLocation();
|
||||||
const ctx = useCtx();
|
const ctx = useCtx();
|
||||||
const { theme, reloadTheme } = useTheme();
|
const { theme, reloadTheme } = useTheme();
|
||||||
const [user, setUser] = useState<any>({});
|
const [user, setUser] = useState<any>({});
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
|
const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
|
||||||
const ws = useRef<any>(null);
|
const ws = useRef<any>(null);
|
||||||
const [socketMessage, setSocketMessage] = useState<any>();
|
const [socketMessage, setSocketMessage] = useState<any>();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const [initLoading, setInitLoading] = useState<boolean>(true);
|
||||||
const {
|
const {
|
||||||
enable: enableDarkMode,
|
enable: enableDarkMode,
|
||||||
disable: disableDarkMode,
|
disable: disableDarkMode,
|
||||||
@@ -53,17 +90,16 @@ export default function (props: any) {
|
|||||||
setSystemInfo(data);
|
setSystemInfo(data);
|
||||||
if (!data.isInitialized) {
|
if (!data.isInitialized) {
|
||||||
history.push('/initialization');
|
history.push('/initialization');
|
||||||
setLoading(false);
|
|
||||||
} else {
|
} else {
|
||||||
|
init(data.version);
|
||||||
getUser();
|
getUser();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
})
|
||||||
|
.finally(() => setInitLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUser = (needLoading = true) => {
|
const getUser = (needLoading = true) => {
|
||||||
@@ -73,11 +109,9 @@ export default function (props: any) {
|
|||||||
.then(({ code, data }) => {
|
.then(({ code, data }) => {
|
||||||
if (code === 200 && data.username) {
|
if (code === 200 && data.username) {
|
||||||
setUser(data);
|
setUser(data);
|
||||||
if (props.location.pathname === '/') {
|
if (location.pathname === '/') {
|
||||||
history.push('/crontab');
|
history.push('/crontab');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
needLoading && setLoading(false);
|
needLoading && setLoading(false);
|
||||||
})
|
})
|
||||||
@@ -94,7 +128,7 @@ export default function (props: any) {
|
|||||||
if (systemInfo && systemInfo.isInitialized && !user) {
|
if (systemInfo && systemInfo.isInitialized && !user) {
|
||||||
getUser();
|
getUser();
|
||||||
}
|
}
|
||||||
}, [props.location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!systemInfo) {
|
if (!systemInfo) {
|
||||||
@@ -112,7 +146,6 @@ export default function (props: any) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
vhCheck();
|
vhCheck();
|
||||||
init();
|
|
||||||
|
|
||||||
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
@@ -136,9 +169,11 @@ export default function (props: any) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user || !user.username) return;
|
||||||
ws.current = new SockJS(
|
ws.current = new SockJS(
|
||||||
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`,
|
`${window.location.origin}/api/ws?token=${localStorage.getItem(
|
||||||
|
config.authKey,
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
ws.current.onmessage = (e: any) => {
|
ws.current.onmessage = (e: any) => {
|
||||||
@@ -146,9 +181,9 @@ export default function (props: any) {
|
|||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
if (data.type === 'ping') {
|
if (data.type === 'ping') {
|
||||||
if (data && data.message === 'hanhh') {
|
if (data && data.message === 'hanhh') {
|
||||||
console.log('websocket连接成功', e);
|
console.log('WS connection succeeded !!!');
|
||||||
} else {
|
} else {
|
||||||
console.log('websocket连接失败', e);
|
console.log('WS connection Failed !!!', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSocketMessage(data);
|
setSocketMessage(data);
|
||||||
@@ -183,29 +218,28 @@ export default function (props: any) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (
|
if (initLoading) {
|
||||||
['/login', '/initialization', '/error'].includes(props.location.pathname)
|
return <PageLoading />;
|
||||||
) {
|
}
|
||||||
document.title = `${(config.documentTitleMap as any)[props.location.pathname]
|
|
||||||
} - 控制面板`;
|
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
|
||||||
if (
|
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
|
||||||
systemInfo?.isInitialized &&
|
|
||||||
props.location.pathname === '/initialization'
|
|
||||||
) {
|
|
||||||
history.push('/crontab');
|
history.push('/crontab');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (systemInfo || props.location.pathname === '/error') {
|
if (systemInfo || location.pathname === '/error') {
|
||||||
return React.Children.map(props.children, (child) => {
|
return (
|
||||||
return React.cloneElement(child, {
|
<Outlet
|
||||||
|
context={{
|
||||||
...ctx,
|
...ctx,
|
||||||
theme,
|
theme,
|
||||||
user,
|
user,
|
||||||
reloadUser,
|
reloadUser,
|
||||||
reloadTheme,
|
reloadTheme,
|
||||||
ws: ws.current,
|
ws: ws.current,
|
||||||
});
|
}}
|
||||||
});
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,32 +249,41 @@ export default function (props: any) {
|
|||||||
!navigator.userAgent.includes('Chrome');
|
!navigator.userAgent.includes('Chrome');
|
||||||
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
|
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
|
||||||
|
|
||||||
const menu = (
|
const menu: MenuProps = {
|
||||||
<Menu
|
items: [
|
||||||
className="side-menu-user-drop-menu"
|
{
|
||||||
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]}
|
label: '退出登录',
|
||||||
onClick={logout}
|
className: 'side-menu-user-drop-menu',
|
||||||
/>
|
onClick: logout,
|
||||||
);
|
key: 'logout',
|
||||||
|
icon: <LogoutOutlined />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
return loading ? (
|
return loading ? (
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
) : (
|
) : (
|
||||||
<ProLayout
|
<ProLayout
|
||||||
selectedKeys={[props.location.pathname]}
|
selectedKeys={[location.pathname]}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
ErrorBoundary={Sentry.ErrorBoundary}
|
ErrorBoundary={Sentry.ErrorBoundary}
|
||||||
logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />}
|
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
|
||||||
|
// @ts-ignore
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<span style={{ fontSize: 16 }}>控制面板</span>
|
<span style={{ fontSize: 16 }}>控制面板</span>
|
||||||
<a
|
<a
|
||||||
href={changeLogLink}
|
href={systemInfo?.changeLogLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
|
||||||
|
>
|
||||||
|
<Badge size="small" dot={systemInfo?.branch === 'develop'}>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: isFirefox ? 9 : 12,
|
fontSize: isFirefox ? 9 : 12,
|
||||||
@@ -250,8 +293,10 @@ export default function (props: any) {
|
|||||||
letterSpacing: isQQBrowser ? -2 : 0,
|
letterSpacing: isQQBrowser ? -2 : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
v{version}
|
v{systemInfo?.version}
|
||||||
</span>
|
</span>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -266,16 +311,15 @@ export default function (props: any) {
|
|||||||
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
|
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
|
||||||
}}
|
}}
|
||||||
pageTitleRender={(props, pageName, info) => {
|
pageTitleRender={(props, pageName, info) => {
|
||||||
if (info && typeof info.pageName === 'string') {
|
const title =
|
||||||
return `${info.pageName} - 控制面板`;
|
(config.documentTitleMap as any)[location.pathname] || '未找到';
|
||||||
}
|
return `${title} - 控制面板`;
|
||||||
return '控制面板';
|
|
||||||
}}
|
}}
|
||||||
onCollapse={setCollapsed}
|
onCollapse={setCollapsed}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
rightContentRender={() =>
|
rightContentRender={() =>
|
||||||
ctx.isPhone && (
|
ctx.isPhone && (
|
||||||
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
|
<Dropdown menu={menu} placement="bottomRight" trigger={['click']}>
|
||||||
<span className="side-menu-user-wrapper">
|
<span className="side-menu-user-wrapper">
|
||||||
<Avatar
|
<Avatar
|
||||||
shape="square"
|
shape="square"
|
||||||
@@ -297,7 +341,7 @@ export default function (props: any) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!collapsed && !ctx.isPhone && (
|
{!collapsed && !ctx.isPhone && (
|
||||||
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}>
|
<Dropdown menu={menu} placement="topLeft" trigger={['hover']}>
|
||||||
<span className="side-menu-user-wrapper">
|
<span className="side-menu-user-wrapper">
|
||||||
<Avatar
|
<Avatar
|
||||||
shape="square"
|
shape="square"
|
||||||
@@ -319,16 +363,17 @@ export default function (props: any) {
|
|||||||
)}
|
)}
|
||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
>
|
>
|
||||||
{React.Children.map(props.children, (child) => {
|
<Outlet
|
||||||
return React.cloneElement(child, {
|
context={{
|
||||||
...ctx,
|
...ctx,
|
||||||
theme,
|
theme,
|
||||||
user,
|
user,
|
||||||
reloadUser,
|
reloadUser,
|
||||||
reloadTheme,
|
reloadTheme,
|
||||||
socketMessage,
|
socketMessage,
|
||||||
});
|
systemInfo,
|
||||||
})}
|
}}
|
||||||
|
/>
|
||||||
</ProLayout>
|
</ProLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Result, Typography } from 'antd';
|
||||||
|
|
||||||
|
const { Link } = Typography;
|
||||||
|
|
||||||
|
const NotFound: React.FC = () => (
|
||||||
|
<Result
|
||||||
|
status="404"
|
||||||
|
title="404"
|
||||||
|
extra={
|
||||||
|
<Button type="primary">
|
||||||
|
<Link href="/">返回首页</Link>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
@@ -11,8 +11,11 @@ import { PageContainer } from '@ant-design/pro-layout';
|
|||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||||
|
import { useOutletContext } from '@umijs/max';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
|
||||||
const Config = ({ headerStyle, isPhone, theme }: any) => {
|
const Config = () => {
|
||||||
|
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [title, setTitle] = useState('config.sh');
|
const [title, setTitle] = useState('config.sh');
|
||||||
@@ -22,8 +25,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
|
||||||
const getConfig = (name: string) => {
|
const getConfig = (name: string) => {
|
||||||
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
|
request.get(`${config.apiPrefix}configs/${name}`).then(({ code, data }) => {
|
||||||
setValue(data.data);
|
if (code === 200) {
|
||||||
|
setValue(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,8 +36,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}configs/files`)
|
.get(`${config.apiPrefix}configs/files`)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
setData(data.data);
|
if (code === 200) {
|
||||||
|
setData(data);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -47,8 +54,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
.post(`${config.apiPrefix}configs/save`, {
|
.post(`${config.apiPrefix}configs/save`, {
|
||||||
data: { content, name: select },
|
data: { content, name: select },
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
message.success(data.message);
|
if (code === 200) {
|
||||||
|
message.success('保存成功');
|
||||||
|
}
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -71,6 +80,7 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
extra={[
|
extra={[
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
|
treeExpandAction="click"
|
||||||
className="config-select"
|
className="config-select"
|
||||||
value={select}
|
value={select}
|
||||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import config from '@/utils/config';
|
|||||||
import CronLogModal from './logModal';
|
import CronLogModal from './logModal';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import IconFont from '@/components/iconfont';
|
import IconFont from '@/components/iconfont';
|
||||||
|
import { getCommandScript } from '@/utils';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -103,7 +104,6 @@ const CronDetailModal = ({
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
fontFamily: 'Source Code Pro',
|
fontFamily: 'Source Code Pro',
|
||||||
folding: false,
|
|
||||||
glyphMargin: false,
|
glyphMargin: false,
|
||||||
wordWrap: 'on',
|
wordWrap: 'on',
|
||||||
}}
|
}}
|
||||||
@@ -123,9 +123,11 @@ const CronDetailModal = ({
|
|||||||
.get(
|
.get(
|
||||||
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
|
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
|
||||||
)
|
)
|
||||||
.then((data) => {
|
.then(({ code, data }) => {
|
||||||
setLog(data.data);
|
if (code === 200) {
|
||||||
|
setLog(data);
|
||||||
setIsLogModalVisible(true);
|
setIsLogModalVisible(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,34 +139,28 @@ const CronDetailModal = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}crons/${cron.id}/logs`)
|
.get(`${config.apiPrefix}crons/${cron.id}/logs`)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
setLogs(data.data);
|
setLogs(data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getScript = () => {
|
const getScript = () => {
|
||||||
const cmd = cron.command.split(' ') as string[];
|
const result = getCommandScript(cron.command);
|
||||||
if (cmd[0] === 'task') {
|
if (Array.isArray(result)) {
|
||||||
setValidTabs(validTabs);
|
setValidTabs(validTabs);
|
||||||
if (cmd[1].startsWith('/ql/data/scripts')) {
|
const [s, p] = result;
|
||||||
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
let [p, s] = cmd[1].split('/');
|
|
||||||
if (!s) {
|
|
||||||
s = p;
|
|
||||||
p = '';
|
|
||||||
}
|
|
||||||
setScriptInfo({ parent: p, filename: s });
|
setScriptInfo({ parent: p, filename: s });
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
|
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
|
||||||
.then((data) => {
|
.then(({ code, data }) => {
|
||||||
setValue(data.data);
|
if (code === 200) {
|
||||||
|
setValue(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else if (result) {
|
||||||
setValidTabs([validTabs[0]]);
|
setValidTabs([validTabs[0]]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -194,12 +190,10 @@ const CronDetailModal = ({
|
|||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((_data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (_data.code === 200) {
|
if (code === 200) {
|
||||||
setValue(content);
|
setValue(content);
|
||||||
message.success(`保存成功`);
|
message.success(`保存成功`);
|
||||||
} else {
|
|
||||||
message.error(_data);
|
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
})
|
})
|
||||||
@@ -227,14 +221,12 @@ const CronDetailModal = ({
|
|||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
|
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
setCurrentCron({ ...currentCron, status: CrontabStatus.running });
|
setCurrentCron({ ...currentCron, status: CrontabStatus.running });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getLogs();
|
getLogs();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -259,11 +251,9 @@ const CronDetailModal = ({
|
|||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
|
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
|
setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -296,14 +286,12 @@ const CronDetailModal = ({
|
|||||||
data: [currentCron.id],
|
data: [currentCron.id],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
setCurrentCron({
|
setCurrentCron({
|
||||||
...currentCron,
|
...currentCron,
|
||||||
isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
|
isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -336,14 +324,12 @@ const CronDetailModal = ({
|
|||||||
data: [currentCron.id],
|
data: [currentCron.id],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
setCurrentCron({
|
setCurrentCron({
|
||||||
...currentCron,
|
...currentCron,
|
||||||
isPinned: currentCron.isPinned === 1 ? 0 : 1,
|
isPinned: currentCron.isPinned === 1 ? 0 : 1,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -378,7 +364,7 @@ const CronDetailModal = ({
|
|||||||
{currentCron.labels?.length > 0 &&
|
{currentCron.labels?.length > 0 &&
|
||||||
currentCron.labels[0] !== '' &&
|
currentCron.labels[0] !== '' &&
|
||||||
currentCron.labels?.map((label: string, i: number) => (
|
currentCron.labels?.map((label: string, i: number) => (
|
||||||
<Tag color="blue" style={{ marginRight: 5 }}>
|
<Tag key={label} color="blue" style={{ marginRight: 5 }}>
|
||||||
{label}
|
{label}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
@@ -441,7 +427,7 @@ const CronDetailModal = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
centered
|
centered
|
||||||
visible={visible}
|
open={visible}
|
||||||
forceRender
|
forceRender
|
||||||
footer={false}
|
footer={false}
|
||||||
onCancel={() => handleCancel()}
|
onCancel={() => handleCancel()}
|
||||||
|
|||||||
@@ -2,21 +2,12 @@
|
|||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cron {
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.crontab-detail {
|
.crontab-detail {
|
||||||
.card-wrapper {
|
.card-wrapper {
|
||||||
height: 80vh;
|
|
||||||
height: calc(80vh - var(--vh-offset, 0px));
|
|
||||||
|
|
||||||
.ant-card:last-child {
|
.ant-card:last-child {
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
height: calc(80vh - 238px);
|
height: calc(80vh - 367px);
|
||||||
height: calc(80vh - var(--vh-offset, 0px) - 238px);
|
height: calc(80vh - var(--vh-offset, 0px) - 367px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,6 +152,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-create-modal-sorts {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.ant-space-item:nth-child(2) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tr.drop-over-downward td {
|
tr.drop-over-downward td {
|
||||||
border-bottom: 2px dashed #1890ff;
|
border-bottom: 2px dashed #1890ff;
|
||||||
}
|
}
|
||||||
@@ -174,3 +173,21 @@ tr.drop-over-upward td {
|
|||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-filters-container.active {
|
||||||
|
.filter-item > div > .ant-form-item-control {
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-modal {
|
||||||
|
.log-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 300px;
|
||||||
|
max-height: calc(80vh - 110px);
|
||||||
|
max-height: calc(80vh - var(--vh-offset, 110px));
|
||||||
|
|
||||||
|
padding: 24px;
|
||||||
|
margin: -24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+248
-257
@@ -1,4 +1,4 @@
|
|||||||
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
message,
|
message,
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
Tabs,
|
Tabs,
|
||||||
TablePaginationConfig,
|
TablePaginationConfig,
|
||||||
|
MenuProps,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
@@ -43,13 +44,16 @@ import CronLogModal from './logModal';
|
|||||||
import CronDetailModal from './detail';
|
import CronDetailModal from './detail';
|
||||||
import cron_parser from 'cron-parser';
|
import cron_parser from 'cron-parser';
|
||||||
import { diffTime } from '@/utils/date';
|
import { diffTime } from '@/utils/date';
|
||||||
import { getTableScroll } from '@/utils/index';
|
import { history, useOutletContext } from '@umijs/max';
|
||||||
import { history } from 'umi';
|
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import ViewCreateModal from './viewCreateModal';
|
import ViewCreateModal from './viewCreateModal';
|
||||||
import ViewManageModal from './viewManageModal';
|
import ViewManageModal from './viewManageModal';
|
||||||
import pagination from 'antd/lib/pagination';
|
|
||||||
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
|
import { getCommandScript, parseCrontab } from '@/utils';
|
||||||
|
import { ColumnProps } from 'antd/lib/table';
|
||||||
|
import { VList } from 'virtuallist-antd';
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -81,8 +85,23 @@ enum OperationPath {
|
|||||||
'unpin',
|
'unpin',
|
||||||
}
|
}
|
||||||
|
|
||||||
const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
export interface ICrontab {
|
||||||
const columns: any = [
|
name: string;
|
||||||
|
command: string;
|
||||||
|
schedule: string;
|
||||||
|
id: number;
|
||||||
|
status: number;
|
||||||
|
isDisabled?: 1 | 0;
|
||||||
|
isPinned?: 1 | 0;
|
||||||
|
labels?: string[];
|
||||||
|
last_running_time?: number;
|
||||||
|
last_execution_time?: number;
|
||||||
|
nextRunTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Crontab = () => {
|
||||||
|
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||||
|
const columns: ColumnProps<ICrontab>[] = [
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
@@ -93,10 +112,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
goToScriptManager(record);
|
setDetailCron(record);
|
||||||
|
setIsDetailModalVisible(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{record.labels?.length > 0 && record.labels[0] !== '' ? (
|
{record.labels?.length > 0 && record.labels[0] !== '' && false ? (
|
||||||
<Popover
|
<Popover
|
||||||
placement="right"
|
placement="right"
|
||||||
trigger={isPhone ? 'click' : 'hover'}
|
trigger={isPhone ? 'click' : 'hover'}
|
||||||
@@ -105,10 +125,12 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
{record.labels?.map((label: string) => (
|
{record.labels?.map((label: string) => (
|
||||||
<Tag
|
<Tag
|
||||||
color="blue"
|
color="blue"
|
||||||
|
key={label}
|
||||||
style={{ cursor: 'point' }}
|
style={{ cursor: 'point' }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setSearchValue(`label:${label}`);
|
setSearchValue(`label:${label}`);
|
||||||
|
setSearchText(`label:${label}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a>{label}</a>
|
<a>{label}</a>
|
||||||
@@ -133,16 +155,16 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
sorter: {
|
sorter: {
|
||||||
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name),
|
compare: (a, b) => a?.name?.localeCompare(b?.name),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '命令',
|
title: '命令/脚本',
|
||||||
dataIndex: 'command',
|
dataIndex: 'command',
|
||||||
key: 'command',
|
key: 'command',
|
||||||
width: 250,
|
width: 300,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (text: string, record: any) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
style={{
|
style={{
|
||||||
@@ -151,8 +173,14 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
}}
|
}}
|
||||||
ellipsis={{ tooltip: text, rows: 2 }}
|
ellipsis={{ tooltip: text, rows: 2 }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
goToScriptManager(record);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
</a>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -167,19 +195,21 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
width: 110,
|
width: 110,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
sorter: {
|
sorter: {
|
||||||
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
|
compare: (a, b) => a.schedule.localeCompare(b.schedule),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '最后运行时间',
|
title: '最后运行时间',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
dataIndex: 'last_execution_time',
|
||||||
|
key: 'last_execution_time',
|
||||||
width: 150,
|
width: 150,
|
||||||
sorter: {
|
sorter: {
|
||||||
compare: (a: any, b: any) => {
|
compare: (a, b) => {
|
||||||
return a.last_execution_time - b.last_execution_time;
|
return (a.last_execution_time || 0) - (b.last_execution_time || 0);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text, record) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
const language = navigator.language || navigator.languages[0];
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -202,12 +232,14 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
title: '最后运行时长',
|
title: '最后运行时长',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
width: 120,
|
width: 120,
|
||||||
|
dataIndex: 'last_running_time',
|
||||||
|
key: 'last_running_time',
|
||||||
sorter: {
|
sorter: {
|
||||||
compare: (a: any, b: any) => {
|
compare: (a: any, b: any) => {
|
||||||
return a.last_running_time - b.last_running_time;
|
return a.last_running_time - b.last_running_time;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text, record) => {
|
||||||
return record.last_running_time
|
return record.last_running_time
|
||||||
? diffTime(record.last_running_time)
|
? diffTime(record.last_running_time)
|
||||||
: '-';
|
: '-';
|
||||||
@@ -222,7 +254,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
return a.nextRunTime - b.nextRunTime;
|
return a.nextRunTime - b.nextRunTime;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text, record) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
const language = navigator.language || navigator.languages[0];
|
||||||
return record.nextRunTime
|
return record.nextRunTime
|
||||||
.toLocaleString(language, {
|
.toLocaleString(language, {
|
||||||
@@ -231,13 +263,12 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
.replace(' 24:', ' 00:');
|
.replace(' 24:', ' 00:');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
width: 85,
|
width: 88,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
text: '运行中',
|
text: '运行中',
|
||||||
@@ -256,14 +287,14 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
value: 3,
|
value: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onFilter: (value: number, record: any) => {
|
onFilter: (value, record) => {
|
||||||
if (record.isDisabled && record.status !== 0) {
|
if (record.isDisabled && record.status !== 0) {
|
||||||
return value === 2;
|
return value === 2;
|
||||||
} else {
|
} else {
|
||||||
return record.status === value;
|
return record.status === value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => (
|
render: (text, record) => (
|
||||||
<>
|
<>
|
||||||
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
|
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
|
||||||
<>
|
<>
|
||||||
@@ -300,7 +331,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
key: 'action',
|
key: 'action',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text, record, index) => {
|
||||||
const isPc = !isPhone;
|
const isPc = !isPhone;
|
||||||
return (
|
return (
|
||||||
<Space size="middle">
|
<Space size="middle">
|
||||||
@@ -358,9 +389,9 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
page: number;
|
page: number;
|
||||||
size: number;
|
size: number;
|
||||||
sorter: any;
|
sorter: any;
|
||||||
|
filters: any;
|
||||||
}>({} as any);
|
}>({} as any);
|
||||||
const [viewConf, setViewConf] = useState<any>();
|
const [viewConf, setViewConf] = useState<any>();
|
||||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
|
||||||
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
|
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
|
||||||
const [detailCron, setDetailCron] = useState<any>();
|
const [detailCron, setDetailCron] = useState<any>();
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
@@ -372,56 +403,55 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
const [cronViews, setCronViews] = useState<any[]>([]);
|
const [cronViews, setCronViews] = useState<any[]>([]);
|
||||||
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
|
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
|
||||||
const [moreMenuActive, setMoreMenuActive] = useState(false);
|
const [moreMenuActive, setMoreMenuActive] = useState(false);
|
||||||
|
const tableRef = useRef<any>();
|
||||||
|
const tableScrollHeight = useTableScrollHeight(tableRef);
|
||||||
|
|
||||||
const goToScriptManager = (record: any) => {
|
const goToScriptManager = (record: any) => {
|
||||||
const cmd = record.command.split(' ') as string[];
|
const result = getCommandScript(record.command);
|
||||||
if (cmd[0] === 'task') {
|
if (Array.isArray(result)) {
|
||||||
if (cmd[1].startsWith('/ql/data/scripts')) {
|
const [s, p] = result;
|
||||||
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
let [p, s] = cmd[1].split('/');
|
|
||||||
if (!s) {
|
|
||||||
s = p;
|
|
||||||
p = '';
|
|
||||||
}
|
|
||||||
history.push(`/script?p=${p}&s=${s}`);
|
history.push(`/script?p=${p}&s=${s}`);
|
||||||
} else if (cmd[1] === 'repo') {
|
} else if (result) {
|
||||||
location.href = cmd[2];
|
location.href = result;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCrons = () => {
|
const getCrons = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { page, size, sorter } = pageConf;
|
const { page, size, sorter, filters } = pageConf;
|
||||||
let url = `${config.apiPrefix}crons?searchValue=${searchText}&page=${page}&size=${size}`;
|
let url = `${
|
||||||
|
config.apiPrefix
|
||||||
|
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
||||||
|
filters,
|
||||||
|
)}`;
|
||||||
if (sorter && sorter.field) {
|
if (sorter && sorter.field) {
|
||||||
url += `&sortField=${sorter.field}&sortType=${
|
url += `&sorter=${JSON.stringify({
|
||||||
sorter.order === 'ascend' ? 'ASC' : 'DESC'
|
field: sorter.field,
|
||||||
}`;
|
type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
|
||||||
|
})}`;
|
||||||
}
|
}
|
||||||
if (viewConf) {
|
if (viewConf) {
|
||||||
url += `&queryString=${JSON.stringify({
|
url += `&queryString=${JSON.stringify({
|
||||||
filters: viewConf.filters,
|
filters: viewConf.filters,
|
||||||
sorts: viewConf.sorts,
|
sorts: viewConf.sorts,
|
||||||
|
filterRelation: viewConf.filterRelation || 'and',
|
||||||
})}`;
|
})}`;
|
||||||
}
|
}
|
||||||
request
|
request
|
||||||
.get(url)
|
.get(url)
|
||||||
.then((_data: any) => {
|
.then(({ code, data: _data }) => {
|
||||||
const { data, total } = _data.data;
|
if (code === 200) {
|
||||||
|
const { data, total } = _data;
|
||||||
setValue(
|
setValue(
|
||||||
data.map((x) => {
|
data.map((x) => {
|
||||||
return {
|
return {
|
||||||
...x,
|
...x,
|
||||||
nextRunTime: cron_parser
|
nextRunTime: parseCrontab(x.schedule),
|
||||||
.parseExpression(x.schedule)
|
|
||||||
.next()
|
|
||||||
.toDate(),
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
setTotal(total);
|
setTotal(total);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -451,8 +481,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.delete(`${config.apiPrefix}crons`, { data: [record.id] })
|
.delete(`${config.apiPrefix}crons`, { data: [record.id] })
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
@@ -460,8 +490,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
result.splice(i, 1);
|
result.splice(i, 1);
|
||||||
setValue(result);
|
setValue(result);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -486,8 +514,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}crons/run`, { data: [record.id] })
|
.put(`${config.apiPrefix}crons/run`, { data: [record.id] })
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
@@ -497,8 +525,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -523,8 +549,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
|
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
@@ -535,8 +561,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -569,8 +593,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
data: [record.id],
|
data: [record.id],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
const newStatus = record.isDisabled === 1 ? 0 : 1;
|
const newStatus = record.isDisabled === 1 ? 0 : 1;
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
@@ -581,8 +605,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -615,8 +637,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
data: [record.id],
|
data: [record.id],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
const newStatus = record.isPinned === 1 ? 0 : 1;
|
const newStatus = record.isPinned === 1 ? 0 : 1;
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
const i = result.findIndex((x) => x.id === record.id);
|
const i = result.findIndex((x) => x.id === record.id);
|
||||||
@@ -627,8 +649,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -664,15 +684,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
arrow={{ pointAtCenter: true }}
|
arrow={{ pointAtCenter: true }}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
overlay={
|
menu={{
|
||||||
<Menu
|
items: getMenuItems(record),
|
||||||
items={getMenuItems(record)}
|
onClick: ({ key, domEvent }) => {
|
||||||
onClick={({ key, domEvent }) => {
|
|
||||||
domEvent.stopPropagation();
|
domEvent.stopPropagation();
|
||||||
action(key, record, index);
|
action(key, record, index);
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<a onClick={(e) => e.stopPropagation()}>
|
<a onClick={(e) => e.stopPropagation()}>
|
||||||
<EllipsisOutlined />
|
<EllipsisOutlined />
|
||||||
@@ -699,67 +717,41 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = (cron?: any) => {
|
const handleCancel = () => {
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
if (cron) {
|
getCrons();
|
||||||
handleCrons(cron);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSearch = (value: string) => {
|
const onSearch = (value: string) => {
|
||||||
setSearchText(value.trim());
|
setSearchText(value.trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCrons = (cron: any) => {
|
|
||||||
const index = value.findIndex((x) => x.id === cron.id);
|
|
||||||
const result = [...value];
|
|
||||||
cron.nextRunTime = cron_parser
|
|
||||||
.parseExpression(cron.schedule)
|
|
||||||
.next()
|
|
||||||
.toDate();
|
|
||||||
if (index === -1) {
|
|
||||||
result.unshift(cron);
|
|
||||||
} else {
|
|
||||||
result.splice(index, 1, {
|
|
||||||
...cron,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setValue(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCronDetail = (cron: any) => {
|
const getCronDetail = (cron: any) => {
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}crons/${cron.id}`)
|
.get(`${config.apiPrefix}crons/${cron.id}`)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
const index = value.findIndex((x) => x.id === cron.id);
|
const index = value.findIndex((x) => x.id === cron.id);
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
data.data.nextRunTime = cron_parser
|
data.nextRunTime = parseCrontab(data.schedule);
|
||||||
.parseExpression(data.data.schedule)
|
|
||||||
.next()
|
|
||||||
.toDate();
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...cron,
|
...cron,
|
||||||
...data.data,
|
...data,
|
||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectChange = (selectedIds: any[]) => {
|
const onSelectChange = (selectedIds: any[]) => {
|
||||||
setSelectedRowIds(selectedIds);
|
setSelectedRowIds(selectedIds);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
|
|
||||||
setTableScrollHeight(getTableScroll());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowSelection = {
|
const rowSelection = {
|
||||||
selectedRowIds,
|
selectedRowKeys: selectedRowIds,
|
||||||
onChange: onSelectChange,
|
onChange: onSelectChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -770,13 +762,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
|
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
message.success('批量删除成功');
|
message.success('批量删除成功');
|
||||||
setSelectedRowIds([]);
|
setSelectedRowIds([]);
|
||||||
getCrons();
|
getCrons();
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -795,11 +785,9 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
|
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
|
||||||
data: selectedRowIds,
|
data: selectedRowIds,
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
getCrons();
|
getCrons();
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -815,7 +803,12 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
sorter: SorterResult<any> | SorterResult<any>[],
|
sorter: SorterResult<any> | SorterResult<any>[],
|
||||||
) => {
|
) => {
|
||||||
const { current, pageSize } = pagination;
|
const { current, pageSize } = pagination;
|
||||||
setPageConf({ page: current as number, size: pageSize as number, sorter });
|
setPageConf({
|
||||||
|
page: current as number,
|
||||||
|
size: pageSize as number,
|
||||||
|
sorter,
|
||||||
|
filters,
|
||||||
|
});
|
||||||
localStorage.setItem('pageSize', String(pageSize));
|
localStorage.setItem('pageSize', String(pageSize));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -842,7 +835,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
|
if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
|
||||||
const view = enabledCronViews.slice(2).find((x) => x.id === viewConf.id);
|
const view = enabledCronViews.slice(4).find((x) => x.id === viewConf.id);
|
||||||
setMoreMenuActive(!!view);
|
setMoreMenuActive(!!view);
|
||||||
}
|
}
|
||||||
}, [viewConf, enabledCronViews]);
|
}, [viewConf, enabledCronViews]);
|
||||||
@@ -852,18 +845,148 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
page: 1,
|
page: 1,
|
||||||
size: parseInt(localStorage.getItem('pageSize') || '20'),
|
size: parseInt(localStorage.getItem('pageSize') || '20'),
|
||||||
sorter: {},
|
sorter: {},
|
||||||
});
|
filters: {},
|
||||||
setTimeout(() => {
|
|
||||||
setTableScrollHeight(getTableScroll());
|
|
||||||
});
|
});
|
||||||
getCronViews();
|
getCronViews();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const panelContent = (
|
const viewAction = (key: string) => {
|
||||||
<>
|
switch (key) {
|
||||||
|
case 'new':
|
||||||
|
setIsCreateViewModalVisible(true);
|
||||||
|
break;
|
||||||
|
case 'manage':
|
||||||
|
setIsViewManageModalVisible(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
tabClick(key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu: MenuProps = {
|
||||||
|
onClick: ({ key, domEvent }) => {
|
||||||
|
domEvent.stopPropagation();
|
||||||
|
viewAction(key);
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
...[...enabledCronViews].slice(4).map((x) => ({
|
||||||
|
label: (
|
||||||
|
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span>{x.name}</span>
|
||||||
|
{viewConf?.id === x.id && (
|
||||||
|
<CheckOutlined style={{ color: '#1890ff' }} />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
key: x.id,
|
||||||
|
icon: <UnorderedListOutlined />,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
type: 'divider' as 'group',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '新建视图',
|
||||||
|
key: 'new',
|
||||||
|
icon: <PlusOutlined />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '视图管理',
|
||||||
|
key: 'manage',
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCronViews = () => {
|
||||||
|
setLoading(true);
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}crons/views`)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
setCronViews(data);
|
||||||
|
setEnabledCronViews(data.filter((x) => !x.isDisabled));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabClick = (key: string) => {
|
||||||
|
const view = enabledCronViews.find((x) => x.id == key);
|
||||||
|
setSelectedRowIds([]);
|
||||||
|
setPageConf({ ...pageConf, page: 1 });
|
||||||
|
setViewConf(view ? view : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const vComponents = useMemo(() => {
|
||||||
|
return VList({
|
||||||
|
height: tableScrollHeight!,
|
||||||
|
resetTopWhenDataChange: false,
|
||||||
|
});
|
||||||
|
}, [tableScrollHeight]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
|
||||||
|
title="定时任务"
|
||||||
|
extra={[
|
||||||
|
<Search
|
||||||
|
placeholder="请输入名称或者关键词"
|
||||||
|
style={{ width: 'auto' }}
|
||||||
|
enterButton
|
||||||
|
allowClear
|
||||||
|
loading={loading}
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>,
|
||||||
|
<Button key="2" type="primary" onClick={() => addCron()}>
|
||||||
|
新建任务
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
header={{
|
||||||
|
style: headerStyle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="all"
|
||||||
|
size="small"
|
||||||
|
tabPosition="top"
|
||||||
|
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
|
||||||
|
tabBarExtraContent={
|
||||||
|
<Dropdown
|
||||||
|
menu={menu}
|
||||||
|
trigger={['click']}
|
||||||
|
overlayStyle={{ minWidth: 200 }}
|
||||||
|
>
|
||||||
|
<div className={`view-more ${moreMenuActive ? 'active' : ''}`}>
|
||||||
|
<Space>
|
||||||
|
更多
|
||||||
|
<DownOutlined />
|
||||||
|
</Space>
|
||||||
|
<div className="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
onTabClick={tabClick}
|
||||||
|
items={[
|
||||||
|
...[...enabledCronViews].slice(0, 4).map((x) => ({
|
||||||
|
key: x.id,
|
||||||
|
label: x.name,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div ref={tableRef}>
|
||||||
{selectedRowIds.length > 0 && (
|
{selectedRowIds.length > 0 && (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginBottom: 5 }}
|
||||||
|
onClick={delCrons}
|
||||||
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -931,14 +1054,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
(a, b) => a - b,
|
(a, b) => a - b,
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
onRow={(record) => {
|
|
||||||
return {
|
|
||||||
onClick: (event) => {
|
|
||||||
setDetailCron(record);
|
|
||||||
setIsDetailModalVisible(true);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
dataSource={value}
|
dataSource={value}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
size="middle"
|
size="middle"
|
||||||
@@ -947,133 +1062,9 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
rowClassName={getRowClassName}
|
rowClassName={getRowClassName}
|
||||||
onChange={onPageChange}
|
onChange={onPageChange}
|
||||||
|
components={vComponents}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewAction = (key: string) => {
|
|
||||||
switch (key) {
|
|
||||||
case 'new':
|
|
||||||
setIsCreateViewModalVisible(true);
|
|
||||||
break;
|
|
||||||
case 'manage':
|
|
||||||
setIsViewManageModalVisible(true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
tabClick(key);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const menu = (
|
|
||||||
<Menu
|
|
||||||
onClick={({ key, domEvent }) => {
|
|
||||||
domEvent.stopPropagation();
|
|
||||||
viewAction(key);
|
|
||||||
}}
|
|
||||||
items={[
|
|
||||||
...[...enabledCronViews].slice(2).map((x) => ({
|
|
||||||
label: (
|
|
||||||
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
||||||
<span>{x.name}</span>
|
|
||||||
{viewConf?.id === x.id && (
|
|
||||||
<CheckOutlined style={{ color: '#1890ff' }} />
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
key: x.id,
|
|
||||||
icon: <UnorderedListOutlined />,
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
type: 'divider',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '新建视图',
|
|
||||||
key: 'new',
|
|
||||||
icon: <PlusOutlined />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '视图管理',
|
|
||||||
key: 'manage',
|
|
||||||
icon: <SettingOutlined />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getCronViews = () => {
|
|
||||||
setLoading(true);
|
|
||||||
request
|
|
||||||
.get(`${config.apiPrefix}crons/views`)
|
|
||||||
.then((data: any) => {
|
|
||||||
setCronViews(data.data);
|
|
||||||
setEnabledCronViews(data.data.filter((x) => !x.isDisabled));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabClick = (key: string) => {
|
|
||||||
const view = enabledCronViews.find((x) => x.id == key);
|
|
||||||
setViewConf(view ? view : null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer
|
|
||||||
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
|
|
||||||
title="定时任务"
|
|
||||||
extra={[
|
|
||||||
<Search
|
|
||||||
placeholder="请输入名称或者关键词"
|
|
||||||
style={{ width: 'auto' }}
|
|
||||||
enterButton
|
|
||||||
allowClear
|
|
||||||
loading={loading}
|
|
||||||
value={searchValue}
|
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
|
||||||
onSearch={onSearch}
|
|
||||||
/>,
|
|
||||||
<Button key="2" type="primary" onClick={() => addCron()}>
|
|
||||||
新建任务
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
header={{
|
|
||||||
style: headerStyle,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs
|
|
||||||
defaultActiveKey="all"
|
|
||||||
size="small"
|
|
||||||
tabPosition="top"
|
|
||||||
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
|
|
||||||
tabBarExtraContent={
|
|
||||||
<Dropdown
|
|
||||||
overlay={menu}
|
|
||||||
trigger={['click']}
|
|
||||||
overlayStyle={{ minWidth: 200 }}
|
|
||||||
>
|
|
||||||
<div className={`view-more ${moreMenuActive ? 'active' : ''}`}>
|
|
||||||
<Space>
|
|
||||||
更多
|
|
||||||
<DownOutlined />
|
|
||||||
</Space>
|
|
||||||
<div className="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
|
||||||
}
|
|
||||||
onTabClick={tabClick}
|
|
||||||
>
|
|
||||||
<Tabs.TabPane tab="全部任务" key="all">
|
|
||||||
{panelContent}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
{[...enabledCronViews].slice(0, 2).map((x) => (
|
|
||||||
<Tabs.TabPane tab={x.name} key={x.id}>
|
|
||||||
{panelContent}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
<CronLogModal
|
<CronLogModal
|
||||||
visible={isLogModalVisible}
|
visible={isLogModalVisible}
|
||||||
handleCancel={() => {
|
handleCancel={() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
|
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { PageLoading } from '@ant-design/pro-layout';
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
|
import { logEnded } from '@/utils';
|
||||||
|
|
||||||
enum CrontabStatus {
|
enum CrontabStatus {
|
||||||
'running',
|
'running',
|
||||||
@@ -33,7 +34,7 @@ const CronLogModal = ({
|
|||||||
const [loading, setLoading] = useState<any>(true);
|
const [loading, setLoading] = useState<any>(true);
|
||||||
const [executing, setExecuting] = useState<any>(true);
|
const [executing, setExecuting] = useState<any>(true);
|
||||||
const [isPhone, setIsPhone] = useState(false);
|
const [isPhone, setIsPhone] = useState(false);
|
||||||
const [theme, setTheme] = useState<string>('');
|
const scrollInfoRef = useRef({ value: 0, down: true });
|
||||||
|
|
||||||
const getCronLog = (isFirst?: boolean) => {
|
const getCronLog = (isFirst?: boolean) => {
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
@@ -41,14 +42,23 @@ const CronLogModal = ({
|
|||||||
}
|
}
|
||||||
request
|
request
|
||||||
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
|
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (localStorage.getItem('logCron') === String(cron.id)) {
|
if (
|
||||||
const log = data.data as string;
|
code === 200 &&
|
||||||
|
localStorage.getItem('logCron') === String(cron.id) &&
|
||||||
|
data !== value
|
||||||
|
) {
|
||||||
|
const log = data as string;
|
||||||
setValue(log || '暂无日志');
|
setValue(log || '暂无日志');
|
||||||
setExecuting(
|
const hasNext = Boolean(
|
||||||
log && !log.includes('执行结束') && !log.includes('重启面板'),
|
log &&
|
||||||
|
!logEnded(log) &&
|
||||||
|
!log.includes('重启面板') &&
|
||||||
|
!log.includes('任务未运行或运行失败,请尝试手动运行'),
|
||||||
);
|
);
|
||||||
if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
|
setExecuting(hasNext);
|
||||||
|
autoScroll();
|
||||||
|
if (hasNext) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getCronLog();
|
getCronLog();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -85,11 +95,33 @@ const CronLogModal = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const autoScroll = () => {
|
||||||
|
if (!scrollInfoRef.current.down) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document
|
||||||
|
.querySelector('#log-flag')!
|
||||||
|
.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
localStorage.removeItem('logCron');
|
localStorage.removeItem('logCron');
|
||||||
handleCancel();
|
handleCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScroll = (e) => {
|
||||||
|
const sTop = e.target.scrollTop;
|
||||||
|
if (scrollInfoRef.current.down) {
|
||||||
|
scrollInfoRef.current = {
|
||||||
|
value: sTop,
|
||||||
|
down: sTop > scrollInfoRef.current.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const titleElement = () => {
|
const titleElement = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -119,7 +151,7 @@ const CronLogModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={titleElement()}
|
title={titleElement()}
|
||||||
visible={visible}
|
open={visible}
|
||||||
centered
|
centered
|
||||||
className="log-modal"
|
className="log-modal"
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
@@ -134,6 +166,7 @@ const CronLogModal = ({
|
|||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
<div onScroll={handleScroll} className="log-container">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
) : (
|
) : (
|
||||||
@@ -151,6 +184,8 @@ const CronLogModal = ({
|
|||||||
{value}
|
{value}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
|
<div id="log-flag"></div>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-10
@@ -28,13 +28,12 @@ const CronModal = ({
|
|||||||
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
|
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
|
||||||
data: payload,
|
data: payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
message.success(cron ? '更新Cron成功' : '新建Cron成功');
|
message.success(cron ? '更新Cron成功' : '新建Cron成功');
|
||||||
} else {
|
handleCancel(data);
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
handleCancel(data);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -47,7 +46,7 @@ const CronModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={cron ? '编辑任务' : '新建任务'}
|
title={cron ? '编辑任务' : '新建任务'}
|
||||||
visible={visible}
|
open={visible}
|
||||||
forceRender
|
forceRender
|
||||||
centered
|
centered
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
@@ -75,13 +74,13 @@ const CronModal = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="command"
|
name="command"
|
||||||
label="命令"
|
label="命令/脚本"
|
||||||
rules={[{ required: true, whitespace: true }]}
|
rules={[{ required: true, whitespace: true }]}
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
autoSize={true}
|
autoSize={true}
|
||||||
placeholder="请输入要执行的命令"
|
placeholder="支持输入脚本路径/任意系统可执行命令/task 脚本路径"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -128,21 +127,24 @@ const CronLabelModal = ({
|
|||||||
.then(async (values) => {
|
.then(async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const payload = { ids, labels: values.labels };
|
const payload = { ids, labels: values.labels };
|
||||||
|
try {
|
||||||
const { code, data } = await request[action](
|
const { code, data } = await request[action](
|
||||||
`${config.apiPrefix}crons/labels`,
|
`${config.apiPrefix}crons/labels`,
|
||||||
{
|
{
|
||||||
data: payload,
|
data: payload,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
message.success(
|
message.success(
|
||||||
action === 'post' ? '添加Labels成功' : '删除Labels成功',
|
action === 'post' ? '添加Labels成功' : '删除Labels成功',
|
||||||
);
|
);
|
||||||
} else {
|
handleCancel(true);
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
handleCancel(true);
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((info) => {
|
.catch((info) => {
|
||||||
console.log('Validate Failed:', info);
|
console.log('Validate Failed:', info);
|
||||||
@@ -166,7 +168,7 @@ const CronLabelModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="批量修改标签"
|
title="批量修改标签"
|
||||||
visible={visible}
|
open={visible}
|
||||||
footer={buttons}
|
footer={buttons}
|
||||||
centered
|
centered
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
|
|||||||
@@ -12,19 +12,28 @@ import {
|
|||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import IconFont from '@/components/iconfont';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
|
||||||
const PROPERTIES = [
|
const PROPERTIES = [
|
||||||
{ name: '命令', value: 'command' },
|
{ name: '命令', value: 'command' },
|
||||||
{ name: '名称', value: 'name' },
|
{ name: '名称', value: 'name' },
|
||||||
{ name: '定时规则', value: 'schedule' },
|
{ name: '定时规则', value: 'schedule' },
|
||||||
{ name: '状态', value: 'status' },
|
{ name: '状态', value: 'status' },
|
||||||
|
{ name: '标签', value: 'labels' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const EOperation: any = {
|
||||||
|
Reg: '',
|
||||||
|
NotReg: '',
|
||||||
|
In: 'select',
|
||||||
|
Nin: 'select',
|
||||||
|
};
|
||||||
const OPERATIONS = [
|
const OPERATIONS = [
|
||||||
{ name: '包含', value: 'Reg' },
|
{ name: '包含', value: 'Reg' },
|
||||||
{ name: '不包含', value: 'NotReg' },
|
{ name: '不包含', value: 'NotReg' },
|
||||||
{ name: '属于', value: 'In' },
|
{ name: '属于', value: 'In', type: 'select' },
|
||||||
{ name: '不属于', value: 'Nin' },
|
{ name: '不属于', value: 'Nin', type: 'select' },
|
||||||
// { name: '等于', value: 'Eq' },
|
// { name: '等于', value: 'Eq' },
|
||||||
// { name: '不等于', value: 'Ne' },
|
// { name: '不等于', value: 'Ne' },
|
||||||
// { name: '为空', value: 'IsNull' },
|
// { name: '为空', value: 'IsNull' },
|
||||||
@@ -36,11 +45,18 @@ const SORTTYPES = [
|
|||||||
{ name: '倒序', value: 'DESC' },
|
{ name: '倒序', value: 'DESC' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const STATUS = [
|
const STATUS_MAP = {
|
||||||
|
status: [
|
||||||
{ name: '运行中', value: 0 },
|
{ name: '运行中', value: 0 },
|
||||||
{ name: '空闲中', value: 1 },
|
{ name: '空闲中', value: 1 },
|
||||||
{ name: '已禁用', value: 2 },
|
{ name: '已禁用', value: 2 },
|
||||||
];
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ViewFilterRelation {
|
||||||
|
'and' = '且',
|
||||||
|
'or' = '或',
|
||||||
|
}
|
||||||
|
|
||||||
const ViewCreateModal = ({
|
const ViewCreateModal = ({
|
||||||
view,
|
view,
|
||||||
@@ -53,10 +69,11 @@ const ViewCreateModal = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [operationMap, setOperationMap] = useState<any>();
|
const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and');
|
||||||
|
|
||||||
const handleOk = async (values: any) => {
|
const handleOk = async (values: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
values.filterRelation = filterRelation;
|
||||||
const method = view ? 'put' : 'post';
|
const method = view ? 'put' : 'post';
|
||||||
try {
|
try {
|
||||||
const { code, data } = await request[method](
|
const { code, data } = await request[method](
|
||||||
@@ -65,11 +82,11 @@ const ViewCreateModal = ({
|
|||||||
data: view ? { ...values, id: view.id } : values,
|
data: view ? { ...values, id: view.id } : values,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (code !== 200) {
|
|
||||||
message.error(data);
|
if (code === 200) {
|
||||||
|
handleCancel(data);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
handleCancel(data);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -87,12 +104,7 @@ const ViewCreateModal = ({
|
|||||||
}, [view, visible]);
|
}, [view, visible]);
|
||||||
|
|
||||||
const operationElement = (
|
const operationElement = (
|
||||||
<Select
|
<Select style={{ width: 80 }}>
|
||||||
style={{ width: 100 }}
|
|
||||||
onChange={() => {
|
|
||||||
setOperationMap({});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{OPERATIONS.map((x) => (
|
{OPERATIONS.map((x) => (
|
||||||
<Select.Option key={x.name} value={x.value}>
|
<Select.Option key={x.name} value={x.value}>
|
||||||
{x.name}
|
{x.name}
|
||||||
@@ -101,9 +113,9 @@ const ViewCreateModal = ({
|
|||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
|
||||||
const propertyElement = (props: any) => {
|
const propertyElement = (props: any, style: React.CSSProperties = {}) => {
|
||||||
return (
|
return (
|
||||||
<Select style={{ width: 120 }}>
|
<Select style={style}>
|
||||||
{props.map((x) => (
|
{props.map((x) => (
|
||||||
<Select.Option key={x.name} value={x.value}>
|
<Select.Option key={x.name} value={x.value}>
|
||||||
{x.name}
|
{x.name}
|
||||||
@@ -114,7 +126,7 @@ const ViewCreateModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const typeElement = (
|
const typeElement = (
|
||||||
<Select style={{ width: 120 }}>
|
<Select>
|
||||||
{SORTTYPES.map((x) => (
|
{SORTTYPES.map((x) => (
|
||||||
<Select.Option key={x.name} value={x.value}>
|
<Select.Option key={x.name} value={x.value}>
|
||||||
{x.name}
|
{x.name}
|
||||||
@@ -123,20 +135,22 @@ const ViewCreateModal = ({
|
|||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
|
||||||
const statusElement = (
|
const statusElement = (property: keyof typeof STATUS_MAP) => {
|
||||||
<Select mode="multiple" allowClear placeholder="请选择状态">
|
return (
|
||||||
{STATUS.map((x) => (
|
<Select mode="tags" allowClear placeholder="输入后回车增加自定义选项">
|
||||||
|
{STATUS_MAP[property]?.map((x) => (
|
||||||
<Select.Option key={x.name} value={x.value}>
|
<Select.Option key={x.name} value={x.value}>
|
||||||
{x.name}
|
{x.name}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={view ? '编辑视图' : '新建视图'}
|
title={view ? '编辑视图' : '新建视图'}
|
||||||
visible={visible}
|
open={visible}
|
||||||
forceRender
|
forceRender
|
||||||
width={580}
|
width={580}
|
||||||
centered
|
centered
|
||||||
@@ -164,20 +178,70 @@ const ViewCreateModal = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.List name="filters">
|
<Form.List name="filters">
|
||||||
{(fields, { add, remove }) => (
|
{(fields, { add, remove }) => (
|
||||||
|
<div
|
||||||
|
style={{ position: 'relative' }}
|
||||||
|
className={`view-filters-container ${
|
||||||
|
fields.length > 1 ? 'active' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{fields.length > 1 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: 50,
|
||||||
|
borderRadius: 10,
|
||||||
|
border: '1px solid rgb(190, 220, 255)',
|
||||||
|
borderRight: 'none',
|
||||||
|
height: 56 * (fields.length - 1),
|
||||||
|
top: 46,
|
||||||
|
left: 15,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
translate: '-50% -50%',
|
||||||
|
padding: '0 0 0 3px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setFilterRelation(
|
||||||
|
filterRelation === 'and' ? 'or' : 'and',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
|
<span>{ViewFilterRelation[filterRelation]}</span>
|
||||||
|
<IconFont type="ql-icon-d-caret" />
|
||||||
|
</>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
{fields.map(({ key, name, ...restField }, index) => (
|
{fields.map(({ key, name, ...restField }, index) => (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={index === 0 ? '筛选条件' : ''}
|
label={index === 0 ? '筛选条件' : ''}
|
||||||
key={key}
|
key={key}
|
||||||
style={{ marginBottom: 0 }}
|
style={{ marginBottom: 0 }}
|
||||||
|
required
|
||||||
|
className="filter-item"
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
className="view-create-modal-filters"
|
||||||
|
align="baseline"
|
||||||
|
style={
|
||||||
|
fields.length > 1 ? { width: 'calc(100% - 40px)' } : {}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Space className="view-create-modal-filters" align="baseline">
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
name={[name, 'property']}
|
name={[name, 'property']}
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
>
|
>
|
||||||
{propertyElement(PROPERTIES)}
|
{propertyElement(PROPERTIES, { width: 90 })}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
@@ -186,19 +250,55 @@ const ViewCreateModal = ({
|
|||||||
>
|
>
|
||||||
{operationElement}
|
{operationElement}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
shouldUpdate={(prevValues, nextValues) => {
|
||||||
|
const preOperation =
|
||||||
|
EOperation[
|
||||||
|
get(prevValues, ['filters', name, 'operation'])
|
||||||
|
];
|
||||||
|
const nextOperation =
|
||||||
|
EOperation[
|
||||||
|
get(nextValues, ['filters', name, 'operation'])
|
||||||
|
];
|
||||||
|
const flag = preOperation !== nextOperation;
|
||||||
|
if (flag) {
|
||||||
|
form.setFieldValue(
|
||||||
|
['filters', name, 'value'],
|
||||||
|
nextOperation === 'select' ? [] : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return flag;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
const property = form.getFieldValue([
|
||||||
|
'filters',
|
||||||
|
index,
|
||||||
|
'property',
|
||||||
|
]) as 'status';
|
||||||
|
const operate = form.getFieldValue([
|
||||||
|
'filters',
|
||||||
|
name,
|
||||||
|
'operation',
|
||||||
|
]);
|
||||||
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
name={[name, 'value']}
|
name={[name, 'value']}
|
||||||
rules={[{ required: true, message: '请输入内容' }]}
|
rules={[
|
||||||
|
{ required: true, message: '请输入内容' },
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{['In', 'Nin'].includes(
|
{EOperation[operate] === 'select' ? (
|
||||||
form.getFieldValue(['filters', index, 'operation']),
|
statusElement(property)
|
||||||
) ? (
|
|
||||||
statusElement
|
|
||||||
) : (
|
) : (
|
||||||
<Input placeholder="请输入内容" />
|
<Input placeholder="请输入内容" />
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
{index !== 0 && (
|
{index !== 0 && (
|
||||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
)}
|
)}
|
||||||
@@ -207,13 +307,16 @@ const ViewCreateModal = ({
|
|||||||
))}
|
))}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<a
|
<a
|
||||||
onClick={() => add({ property: 'command', operation: 'Reg' })}
|
onClick={() =>
|
||||||
|
add({ property: 'command', operation: 'Reg' })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
新增筛选条件
|
新增筛选条件
|
||||||
</a>
|
</a>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
<Form.List name="sorts">
|
<Form.List name="sorts">
|
||||||
@@ -225,13 +328,13 @@ const ViewCreateModal = ({
|
|||||||
key={key}
|
key={key}
|
||||||
style={{ marginBottom: 0 }}
|
style={{ marginBottom: 0 }}
|
||||||
>
|
>
|
||||||
<Space className="view-create-modal-filters" align="baseline">
|
<Space className="view-create-modal-sorts" align="baseline">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
name={[name, 'property']}
|
name={[name, 'property']}
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
>
|
>
|
||||||
{propertyElement(PROPERTIES)}
|
{propertyElement(PROPERTIES, { width: 240 })}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
@@ -240,9 +343,7 @@ const ViewCreateModal = ({
|
|||||||
>
|
>
|
||||||
{typeElement}
|
{typeElement}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{index !== 0 && (
|
|
||||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -81,6 +81,16 @@ const ViewManageModal = ({
|
|||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
render: (text) => (
|
||||||
|
<div style={{ textAlign: 'left', paddingLeft: 30 }}>{text}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
key: 'type',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (v) => (v === 1 ? '系统' : '个人'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '显示',
|
title: '显示',
|
||||||
@@ -100,10 +110,10 @@ const ViewManageModal = ({
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 140,
|
width: 100,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
return (
|
return record.type !== 1 ? (
|
||||||
<Space size="middle">
|
<Space size="middle">
|
||||||
<a onClick={() => editView(record, index)}>
|
<a onClick={() => editView(record, index)}>
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
@@ -112,6 +122,8 @@ const ViewManageModal = ({
|
|||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</a>
|
</a>
|
||||||
</Space>
|
</Space>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -141,12 +153,10 @@ const ViewManageModal = ({
|
|||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.delete(`${config.apiPrefix}crons/views`, { data: [record.id] })
|
.delete(`${config.apiPrefix}crons/views`, { data: [record.id] })
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
cronViewChange();
|
cronViewChange();
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -157,19 +167,16 @@ const ViewManageModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onShowChange = (checked: boolean, record: any, index: number) => {
|
const onShowChange = (checked: boolean, record: any, index: number) => {
|
||||||
console.log(checked);
|
|
||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
|
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
|
||||||
data: [record.id],
|
data: [record.id],
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
const _list = [...list];
|
const _list = [...list];
|
||||||
_list.splice(index, 1, { ...list[index], isDisabled: !checked });
|
_list.splice(index, 1, { ...list[index], isDisabled: !checked });
|
||||||
setList(_list);
|
setList(_list);
|
||||||
cronViewChange();
|
cronViewChange();
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -190,15 +197,13 @@ const ViewManageModal = ({
|
|||||||
.put(`${config.apiPrefix}crons/views/move`, {
|
.put(`${config.apiPrefix}crons/views/move`, {
|
||||||
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
|
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
const newData = [...list];
|
const newData = [...list];
|
||||||
newData.splice(dragIndex, 1);
|
newData.splice(dragIndex, 1);
|
||||||
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
|
newData.splice(hoverIndex, 0, { ...dragRow, ...data });
|
||||||
setList(newData);
|
setList(newData);
|
||||||
cronViewChange();
|
cronViewChange();
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -212,7 +217,7 @@ const ViewManageModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="视图管理"
|
title="视图管理"
|
||||||
visible={visible}
|
open={visible}
|
||||||
centered
|
centered
|
||||||
width={620}
|
width={620}
|
||||||
onCancel={() => handleCancel()}
|
onCancel={() => handleCancel()}
|
||||||
@@ -231,7 +236,10 @@ const ViewManageModal = ({
|
|||||||
<Button
|
<Button
|
||||||
key="2"
|
key="2"
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => setIsCreateViewModalVisible(true)}
|
onClick={() => {
|
||||||
|
setEditedView(null);
|
||||||
|
setIsCreateViewModalVisible(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
新建视图
|
新建视图
|
||||||
</Button>
|
</Button>
|
||||||
@@ -258,8 +266,8 @@ const ViewManageModal = ({
|
|||||||
view={editedView}
|
view={editedView}
|
||||||
visible={isCreateViewModalVisible}
|
visible={isCreateViewModalVisible}
|
||||||
handleCancel={(data) => {
|
handleCancel={(data) => {
|
||||||
cronViewChange(data);
|
|
||||||
setIsCreateViewModalVisible(false);
|
setIsCreateViewModalVisible(false);
|
||||||
|
cronViewChange(data);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
+101
-80
@@ -27,8 +27,11 @@ import DependenceModal from './modal';
|
|||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import { getTableScroll } from '@/utils/index';
|
|
||||||
import DependenceLogModal from './logModal';
|
import DependenceLogModal from './logModal';
|
||||||
|
import { useOutletContext } from '@umijs/max';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -48,7 +51,9 @@ enum StatusColor {
|
|||||||
'error',
|
'error',
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
const Dependence = () => {
|
||||||
|
const { headerStyle, isPhone, socketMessage } =
|
||||||
|
useOutletContext<SharedContext>();
|
||||||
const columns: any = [
|
const columns: any = [
|
||||||
{
|
{
|
||||||
title: '序号',
|
title: '序号',
|
||||||
@@ -160,10 +165,11 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
const [editedDependence, setEditedDependence] = useState();
|
const [editedDependence, setEditedDependence] = useState();
|
||||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
|
||||||
const [logDependence, setLogDependence] = useState<any>();
|
const [logDependence, setLogDependence] = useState<any>();
|
||||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||||
const [type, setType] = useState('nodejs');
|
const [type, setType] = useState('nodejs');
|
||||||
|
const tableRef = useRef<any>();
|
||||||
|
const tableScrollHeight = useTableScrollHeight(tableRef, 59)
|
||||||
|
|
||||||
const getDependencies = () => {
|
const getDependencies = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -171,8 +177,10 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
.get(
|
.get(
|
||||||
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
|
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
|
||||||
)
|
)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
setValue(data.data);
|
if (code === 200) {
|
||||||
|
setValue(data);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -208,9 +216,8 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
.delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {
|
.delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {
|
||||||
data: [record.id],
|
data: [record.id],
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200 && force) {
|
||||||
if (force) {
|
|
||||||
const i = value.findIndex((x) => x.id === data.data[0].id);
|
const i = value.findIndex((x) => x.id === data.data[0].id);
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
@@ -218,9 +225,6 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
setValue(result);
|
setValue(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onCancel() {
|
onCancel() {
|
||||||
@@ -246,11 +250,9 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
.put(`${config.apiPrefix}dependencies/reinstall`, {
|
.put(`${config.apiPrefix}dependencies/reinstall`, {
|
||||||
data: [record.id],
|
data: [record.id],
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
handleDependence(data.data[0]);
|
handleDependence(data[0]);
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -282,16 +284,10 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
|
|
||||||
const onSelectChange = (selectedIds: any[]) => {
|
const onSelectChange = (selectedIds: any[]) => {
|
||||||
setSelectedRowIds(selectedIds);
|
setSelectedRowIds(selectedIds);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
|
|
||||||
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowSelection = {
|
const rowSelection = {
|
||||||
selectedRowIds,
|
selectedRowKeys: selectedRowIds,
|
||||||
onChange: onSelectChange,
|
onChange: onSelectChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -305,12 +301,32 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
.delete(`${config.apiPrefix}dependencies${forceUrl}`, {
|
.delete(`${config.apiPrefix}dependencies${forceUrl}`, {
|
||||||
data: selectedRowIds,
|
data: selectedRowIds,
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
|
setSelectedRowIds([]);
|
||||||
|
getDependencies();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlereInstallDependencies = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认重新安装',
|
||||||
|
content: <>确认重新安装选中的依赖吗</>,
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}dependencies/reinstall`, {
|
||||||
|
data: selectedRowIds,
|
||||||
|
})
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
setSelectedRowIds([]);
|
setSelectedRowIds([]);
|
||||||
getDependencies();
|
getDependencies();
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -323,16 +339,18 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
const getDependenceDetail = (dependence: any) => {
|
const getDependenceDetail = (dependence: any) => {
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
|
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
const index = value.findIndex((x) => x.id === dependence.id);
|
const index = value.findIndex((x) => x.id === dependence.id);
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...dependence,
|
...dependence,
|
||||||
...data.data,
|
...data,
|
||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -345,12 +363,6 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
getDependencies();
|
getDependencies();
|
||||||
}, [searchText, type]);
|
}, [searchText, type]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (logDependence) {
|
if (logDependence) {
|
||||||
localStorage.setItem('logDependence', logDependence.id);
|
localStorage.setItem('logDependence', logDependence.id);
|
||||||
@@ -399,10 +411,61 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
}
|
}
|
||||||
}, [socketMessage]);
|
}, [socketMessage]);
|
||||||
|
|
||||||
const panelContent = () => (
|
const onTabChange = (activeKey: string) => {
|
||||||
<>
|
setSelectedRowIds([]);
|
||||||
|
setType(activeKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
className="ql-container-wrapper dependence-wrapper ql-container-wrapper-has-tab"
|
||||||
|
title="依赖管理"
|
||||||
|
extra={[
|
||||||
|
<Search
|
||||||
|
placeholder="请输入名称"
|
||||||
|
style={{ width: 'auto' }}
|
||||||
|
enterButton
|
||||||
|
loading={loading}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>,
|
||||||
|
<Button key="2" type="primary" onClick={() => addDependence()}>
|
||||||
|
新建依赖
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
header={{
|
||||||
|
style: headerStyle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="nodejs"
|
||||||
|
size="small"
|
||||||
|
tabPosition="top"
|
||||||
|
onChange={onTabChange}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'nodejs',
|
||||||
|
label: 'NodeJs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'python3',
|
||||||
|
label: 'Python3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'linux',
|
||||||
|
label: 'Linux',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div ref={tableRef}>
|
||||||
{selectedRowIds.length > 0 && (
|
{selectedRowIds.length > 0 && (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginBottom: 5, marginLeft: 8 }}
|
||||||
|
onClick={() => handlereInstallDependencies()}
|
||||||
|
>
|
||||||
|
批量安装
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
style={{ marginBottom: 5, marginLeft: 8 }}
|
style={{ marginBottom: 5, marginLeft: 8 }}
|
||||||
@@ -435,49 +498,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</DndProvider>
|
</DndProvider>
|
||||||
</>
|
</div>
|
||||||
);
|
|
||||||
|
|
||||||
const onTabChange = (activeKey: string) => {
|
|
||||||
setType(activeKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer
|
|
||||||
className="ql-container-wrapper dependence-wrapper ql-container-wrapper-has-tab"
|
|
||||||
title="依赖管理"
|
|
||||||
extra={[
|
|
||||||
<Search
|
|
||||||
placeholder="请输入名称"
|
|
||||||
style={{ width: 'auto' }}
|
|
||||||
enterButton
|
|
||||||
loading={loading}
|
|
||||||
onSearch={onSearch}
|
|
||||||
/>,
|
|
||||||
<Button key="2" type="primary" onClick={() => addDependence()}>
|
|
||||||
新建依赖
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
header={{
|
|
||||||
style: headerStyle,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs
|
|
||||||
defaultActiveKey="nodejs"
|
|
||||||
size="small"
|
|
||||||
tabPosition="top"
|
|
||||||
onChange={onTabChange}
|
|
||||||
>
|
|
||||||
<Tabs.TabPane tab="NodeJs" key="nodejs">
|
|
||||||
{panelContent()}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
<Tabs.TabPane tab="Python3" key="python3">
|
|
||||||
{panelContent()}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
<Tabs.TabPane tab="Linux" key="linux">
|
|
||||||
{panelContent()}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
<DependenceModal
|
<DependenceModal
|
||||||
visible={isModalVisible}
|
visible={isModalVisible}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
|
|||||||
@@ -48,9 +48,12 @@ const DependenceLogModal = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
|
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (localStorage.getItem('logDependence') === String(dependence.id)) {
|
if (
|
||||||
const log = (data.data.log || []).join('') as string;
|
code === 200 &&
|
||||||
|
localStorage.getItem('logDependence') === String(dependence.id)
|
||||||
|
) {
|
||||||
|
const log = (data.log || []).join('') as string;
|
||||||
setValue(log);
|
setValue(log);
|
||||||
setExecuting(!log.includes('结束时间'));
|
setExecuting(!log.includes('结束时间'));
|
||||||
setIsRemoveFailed(log.includes('删除失败'));
|
setIsRemoveFailed(log.includes('删除失败'));
|
||||||
@@ -67,8 +70,10 @@ const DependenceLogModal = ({
|
|||||||
.delete(`${config.apiPrefix}dependencies/force`, {
|
.delete(`${config.apiPrefix}dependencies/force`, {
|
||||||
data: [dependence.id],
|
data: [dependence.id],
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
cancel(true);
|
cancel(true);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setRemoveLoading(false);
|
setRemoveLoading(false);
|
||||||
@@ -112,7 +117,7 @@ const DependenceLogModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={titleElement()}
|
title={titleElement()}
|
||||||
visible={visible}
|
open={visible}
|
||||||
centered
|
centered
|
||||||
className="log-modal"
|
className="log-modal"
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ const DependenceModal = ({
|
|||||||
data: payload,
|
data: payload,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (code !== 200) {
|
|
||||||
message.error(data);
|
if (code === 200) {
|
||||||
|
handleCancel(data);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
handleCancel(data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ const DependenceModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={dependence ? '编辑依赖' : '新建依赖'}
|
title={dependence ? '编辑依赖' : '新建依赖'}
|
||||||
visible={visible}
|
open={visible}
|
||||||
forceRender
|
forceRender
|
||||||
centered
|
centered
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
@@ -99,7 +99,9 @@ const DependenceModal = ({
|
|||||||
>
|
>
|
||||||
<Select>
|
<Select>
|
||||||
{config.dependenceTypes.map((x, i) => (
|
{config.dependenceTypes.map((x, i) => (
|
||||||
<Option value={i}>{x}</Option>
|
<Option key={i} value={i}>
|
||||||
|
{x}
|
||||||
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -17,4 +17,8 @@
|
|||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ section {
|
||||||
|
height: calc(100% - 40px) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-11
@@ -6,10 +6,13 @@ import { request } from '@/utils/http';
|
|||||||
import './index.less';
|
import './index.less';
|
||||||
import { DiffEditor } from '@monaco-editor/react';
|
import { DiffEditor } from '@monaco-editor/react';
|
||||||
import ReactDiffViewer from 'react-diff-viewer';
|
import ReactDiffViewer from 'react-diff-viewer';
|
||||||
|
import { useOutletContext } from '@umijs/max';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
const Diff = ({ headerStyle, isPhone, theme }: any) => {
|
const Diff = () => {
|
||||||
|
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||||
const [origin, setOrigin] = useState('config.sample.sh');
|
const [origin, setOrigin] = useState('config.sample.sh');
|
||||||
const [current, setCurrent] = useState('config.sh');
|
const [current, setCurrent] = useState('config.sh');
|
||||||
const [originValue, setOriginValue] = useState('');
|
const [originValue, setOriginValue] = useState('');
|
||||||
@@ -19,14 +22,22 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
const getConfig = () => {
|
const getConfig = () => {
|
||||||
request.get(`${config.apiPrefix}configs/${current}`).then((data) => {
|
request
|
||||||
setCurrentValue(data.data);
|
.get(`${config.apiPrefix}configs/${current}`)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
setCurrentValue(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSample = () => {
|
const getSample = () => {
|
||||||
request.get(`${config.apiPrefix}configs/${origin}`).then((data) => {
|
request
|
||||||
setOriginValue(data.data);
|
.get(`${config.apiPrefix}configs/${origin}`)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
setOriginValue(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,8 +50,10 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
.post(`${config.apiPrefix}configs/save`, {
|
.post(`${config.apiPrefix}configs/save`, {
|
||||||
data: { content, name: current },
|
data: { content, name: current },
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
message.success(data.message);
|
if (code === 200) {
|
||||||
|
message.success('保存成功');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,8 +61,10 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}configs/files`)
|
.get(`${config.apiPrefix}configs/files`)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
setFiles(data.data);
|
if (code === 200) {
|
||||||
|
setFiles(data);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -95,7 +110,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
<Form.Item label="源文件">
|
<Form.Item label="源文件">
|
||||||
<Select value={origin} onChange={originFileChange}>
|
<Select value={origin} onChange={originFileChange}>
|
||||||
{files.map((x) => (
|
{files.map((x) => (
|
||||||
<Option value={x.value}>{x.title}</Option>
|
<Option key={x.value} value={x.value}>
|
||||||
|
{x.title}
|
||||||
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -104,7 +121,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
<Form.Item label="当前文件">
|
<Form.Item label="当前文件">
|
||||||
<Select value={current} onChange={currentFileChange}>
|
<Select value={current} onChange={currentFileChange}>
|
||||||
{files.map((x) => (
|
{files.map((x) => (
|
||||||
<Option value={x.value}>{x.title}</Option>
|
<Option key={x.value} value={x.value}>
|
||||||
|
{x.title}
|
||||||
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
Vendored
+7
-4
@@ -17,19 +17,22 @@ const EditNameModal = ({
|
|||||||
|
|
||||||
const handleOk = async (values: any) => {
|
const handleOk = async (values: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
try {
|
||||||
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
|
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
|
||||||
data: {
|
data: {
|
||||||
ids,
|
ids,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
message.success('更新环境变量名称成功');
|
message.success('更新环境变量名称成功');
|
||||||
} else {
|
handleCancel();
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
handleCancel();
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,7 +42,7 @@ const EditNameModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="修改环境变量名称"
|
title="修改环境变量名称"
|
||||||
visible={visible}
|
open={visible}
|
||||||
forceRender
|
forceRender
|
||||||
centered
|
centered
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
|
|||||||
Vendored
+6
@@ -5,3 +5,9 @@ tr.drop-over-downward td {
|
|||||||
tr.drop-over-upward td {
|
tr.drop-over-upward td {
|
||||||
border-top: 2px dashed #1890ff;
|
border-top: 2px dashed #1890ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-ellipsis {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+144
-108
@@ -1,4 +1,10 @@
|
|||||||
import React, { useCallback, useRef, useState, useEffect } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
message,
|
message,
|
||||||
@@ -9,6 +15,8 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Input,
|
Input,
|
||||||
|
UploadProps,
|
||||||
|
Upload,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -16,6 +24,7 @@ import {
|
|||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
StopOutlined,
|
StopOutlined,
|
||||||
|
UploadOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
@@ -25,9 +34,14 @@ import EditNameModal from './editNameModal';
|
|||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import { exportJson, getTableScroll } from '@/utils/index';
|
import { exportJson } from '@/utils/index';
|
||||||
|
import { useOutletContext } from '@umijs/max';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
|
import Copy from '../../components/copy';
|
||||||
|
import { VList } from 'virtuallist-antd';
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
enum Status {
|
enum Status {
|
||||||
@@ -52,51 +66,8 @@ enum OperationPath {
|
|||||||
|
|
||||||
const type = 'DragableBodyRow';
|
const type = 'DragableBodyRow';
|
||||||
|
|
||||||
const DragableBodyRow = ({
|
const Env = () => {
|
||||||
index,
|
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||||
moveRow,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
...restProps
|
|
||||||
}: any) => {
|
|
||||||
const ref = useRef();
|
|
||||||
const [{ isOver, dropClassName }, drop] = useDrop({
|
|
||||||
accept: type,
|
|
||||||
collect: (monitor) => {
|
|
||||||
const { index: dragIndex } = (monitor.getItem() as any) || {};
|
|
||||||
if (dragIndex === index) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isOver: monitor.isOver(),
|
|
||||||
dropClassName:
|
|
||||||
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
drop: (item: any) => {
|
|
||||||
moveRow(item.index, index);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [, drag] = useDrag({
|
|
||||||
type,
|
|
||||||
item: { index },
|
|
||||||
collect: (monitor) => ({
|
|
||||||
isDragging: monitor.isDragging(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
drop(drag(ref));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
ref={ref}
|
|
||||||
className={`${className}${isOver ? dropClassName : ''}`}
|
|
||||||
style={{ cursor: 'move', ...style }}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|
||||||
const columns: any = [
|
const columns: any = [
|
||||||
{
|
{
|
||||||
title: '序号',
|
title: '序号',
|
||||||
@@ -121,16 +92,12 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
width: '35%',
|
width: '35%',
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
style={{
|
<Tooltip title={text} placement="topLeft">
|
||||||
wordBreak: 'break-all',
|
<div className="text-ellipsis">{text}</div>
|
||||||
marginBottom: 0,
|
</Tooltip>
|
||||||
textAlign: 'left',
|
<Copy text={text} />
|
||||||
}}
|
</div>
|
||||||
ellipsis={{ tooltip: text, rows: 2 }}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Paragraph>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -139,6 +106,13 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
dataIndex: 'remarks',
|
dataIndex: 'remarks',
|
||||||
key: 'remarks',
|
key: 'remarks',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return (
|
||||||
|
<Tooltip title={text} placement="topLeft">
|
||||||
|
<div className="text-ellipsis">{text}</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '更新时间',
|
title: '更新时间',
|
||||||
@@ -246,14 +220,18 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
const [editedEnv, setEditedEnv] = useState();
|
const [editedEnv, setEditedEnv] = useState();
|
||||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
const [importLoading, setImportLoading] = useState(false);
|
||||||
|
const tableRef = useRef<any>();
|
||||||
|
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
|
||||||
|
|
||||||
const getEnvs = () => {
|
const getEnvs = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}envs?searchValue=${searchText}`)
|
.get(`${config.apiPrefix}envs?searchValue=${searchText}`)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
setValue(data.data);
|
if (code === 200) {
|
||||||
|
setValue(data);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -281,8 +259,8 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
data: [record.id],
|
data: [record.id],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
message.success(
|
message.success(
|
||||||
`${record.status === Status.已禁用 ? '启用' : '禁用'}成功`,
|
`${record.status === Status.已禁用 ? '启用' : '禁用'}成功`,
|
||||||
);
|
);
|
||||||
@@ -294,8 +272,6 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
status: newStatus,
|
status: newStatus,
|
||||||
});
|
});
|
||||||
setValue(result);
|
setValue(result);
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -330,14 +306,12 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.delete(`${config.apiPrefix}envs`, { data: [record.id] })
|
.delete(`${config.apiPrefix}envs`, { data: [record.id] })
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
result.splice(index, 1);
|
result.splice(index, 1);
|
||||||
setValue(result);
|
setValue(result);
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -349,7 +323,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
|
|
||||||
const handleCancel = (env?: any[]) => {
|
const handleCancel = (env?: any[]) => {
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
env && handleEnv(env);
|
getEnvs();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditNameCancel = (env?: any[]) => {
|
const handleEditNameCancel = (env?: any[]) => {
|
||||||
@@ -357,28 +331,71 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
getEnvs();
|
getEnvs();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnv = (env: any) => {
|
const vComponents = useMemo(() => {
|
||||||
const result = [...value];
|
return VList({
|
||||||
const index = value.findIndex((x) => x.id === env.id);
|
height: tableScrollHeight!,
|
||||||
if (index === -1) {
|
resetTopWhenDataChange: false,
|
||||||
env = Array.isArray(env) ? env : [env];
|
|
||||||
result.push(...env);
|
|
||||||
} else {
|
|
||||||
result.splice(index, 1, {
|
|
||||||
...env,
|
|
||||||
});
|
});
|
||||||
|
}, [tableScrollHeight]);
|
||||||
|
|
||||||
|
const DragableBodyRow = (props: any) => {
|
||||||
|
const { index, moveRow, className, style, ...restProps } = props;
|
||||||
|
const ref = useRef();
|
||||||
|
const [{ isOver, dropClassName }, drop] = useDrop({
|
||||||
|
accept: type,
|
||||||
|
collect: (monitor) => {
|
||||||
|
const { index: dragIndex } = (monitor.getItem() as any) || {};
|
||||||
|
if (dragIndex === index) {
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
setValue(result);
|
return {
|
||||||
|
isOver: monitor.isOver(),
|
||||||
|
dropClassName:
|
||||||
|
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
drop: (item: any) => {
|
||||||
|
moveRow(item.index, index);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [, drag] = useDrag({
|
||||||
|
type,
|
||||||
|
item: { index },
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
drop(drag(ref));
|
||||||
|
}, [drag, drop]);
|
||||||
|
|
||||||
|
const components = useMemo(() => vComponents.body.row, []);
|
||||||
|
|
||||||
|
const tempProps = useMemo(() => {
|
||||||
|
return {
|
||||||
|
ref: ref,
|
||||||
|
className: `${className}${isOver ? dropClassName : ''}`,
|
||||||
|
style: { cursor: 'move', ...style },
|
||||||
|
...restProps,
|
||||||
|
};
|
||||||
|
}, [className, dropClassName, restProps, style, isOver]);
|
||||||
|
|
||||||
|
return <> {components(tempProps, ref)} </>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const components = {
|
const components = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...vComponents,
|
||||||
body: {
|
body: {
|
||||||
|
...vComponents.body,
|
||||||
row: DragableBodyRow,
|
row: DragableBodyRow,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}, [vComponents]);
|
||||||
|
|
||||||
const moveRow = useCallback(
|
const moveRow = useCallback(
|
||||||
(dragIndex, hoverIndex) => {
|
(dragIndex: number, hoverIndex: number) => {
|
||||||
if (dragIndex === hoverIndex) {
|
if (dragIndex === hoverIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -387,14 +404,12 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
|
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
|
||||||
data: { fromIndex: dragIndex, toIndex: hoverIndex },
|
data: { fromIndex: dragIndex, toIndex: hoverIndex },
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
const newData = [...value];
|
const newData = [...value];
|
||||||
newData.splice(dragIndex, 1);
|
newData.splice(dragIndex, 1);
|
||||||
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
|
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
|
||||||
setValue([...newData]);
|
setValue([...newData]);
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -403,16 +418,10 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
|
|
||||||
const onSelectChange = (selectedIds: any[]) => {
|
const onSelectChange = (selectedIds: any[]) => {
|
||||||
setSelectedRowIds(selectedIds);
|
setSelectedRowIds(selectedIds);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
|
|
||||||
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowSelection = {
|
const rowSelection = {
|
||||||
selectedRowIds,
|
selectedRowKeys: selectedRowIds,
|
||||||
onChange: onSelectChange,
|
onChange: onSelectChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -423,13 +432,11 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
|
.delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
message.success('批量删除成功');
|
message.success('批量删除成功');
|
||||||
setSelectedRowIds([]);
|
setSelectedRowIds([]);
|
||||||
getEnvs();
|
getEnvs();
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -448,11 +455,9 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
.put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
|
.put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
|
||||||
data: selectedRowIds,
|
data: selectedRowIds,
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
getEnvs();
|
getEnvs();
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -477,16 +482,37 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
setSearchText(value.trim());
|
setSearchText(value.trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uploadProps: UploadProps = {
|
||||||
|
accept: 'application/json',
|
||||||
|
beforeUpload: async (file) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('env', file);
|
||||||
|
setImportLoading(true);
|
||||||
|
try {
|
||||||
|
const { code, data } = await request.post(
|
||||||
|
`${config.apiPrefix}envs/upload`,
|
||||||
|
{
|
||||||
|
data: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (code === 200) {
|
||||||
|
message.success(`成功上传${data.length}个环境变量`);
|
||||||
|
getEnvs();
|
||||||
|
}
|
||||||
|
setImportLoading(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
setImportLoading(false);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
fileList: [],
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getEnvs();
|
getEnvs();
|
||||||
}, [searchText]);
|
}, [searchText]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="ql-container-wrapper env-wrapper"
|
className="ql-container-wrapper env-wrapper"
|
||||||
@@ -499,6 +525,15 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
/>,
|
/>,
|
||||||
|
<Upload {...uploadProps}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<UploadOutlined />}
|
||||||
|
loading={importLoading}
|
||||||
|
>
|
||||||
|
导入
|
||||||
|
</Button>
|
||||||
|
</Upload>,
|
||||||
<Button key="2" type="primary" onClick={() => addEnv()}>
|
<Button key="2" type="primary" onClick={() => addEnv()}>
|
||||||
新建变量
|
新建变量
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -552,6 +587,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
)}
|
)}
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<Table
|
<Table
|
||||||
|
ref={tableRef}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
@@ -561,7 +597,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
scroll={{ x: 1000, y: tableScrollHeight }}
|
scroll={{ x: 1000, y: tableScrollHeight }}
|
||||||
components={components}
|
components={components}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onRow={(record: any, index: number) => {
|
onRow={(record: any, index: number | undefined) => {
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
moveRow,
|
moveRow,
|
||||||
|
|||||||
Vendored
+3
-4
@@ -40,13 +40,12 @@ const EnvModal = ({
|
|||||||
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
|
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
|
||||||
data: payload,
|
data: payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
message.success(env ? '更新变量成功' : '新建变量成功');
|
message.success(env ? '更新变量成功' : '新建变量成功');
|
||||||
} else {
|
handleCancel(data);
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
handleCancel(data);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -59,7 +58,7 @@ const EnvModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={env ? '编辑变量' : '新建变量'}
|
title={env ? '编辑变量' : '新建变量'}
|
||||||
visible={visible}
|
open={visible}
|
||||||
forceRender
|
forceRender
|
||||||
centered
|
centered
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
|
|||||||
@@ -3,22 +3,43 @@ import config from '@/utils/config';
|
|||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import Terminal, { ColorMode, LineType } from '../../components/terminal';
|
import Terminal, { ColorMode, LineType } from '../../components/terminal';
|
||||||
import { PageLoading } from '@ant-design/pro-layout';
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
import { history } from 'umi';
|
import { history, useOutletContext } from '@umijs/max';
|
||||||
import Ansi from 'ansi-to-react';
|
import Ansi from 'ansi-to-react';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
|
||||||
const Error = ({ user, theme }: any) => {
|
const Error = () => {
|
||||||
|
const { user, theme, reloadUser } = useOutletContext<SharedContext>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState('暂无日志');
|
const [data, setData] = useState('暂无日志');
|
||||||
|
|
||||||
const getLog = () => {
|
const getTimes = () => {
|
||||||
setLoading(true);
|
return parseInt(localStorage.getItem('error_retry_times') || '0', 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
let times = getTimes();
|
||||||
|
|
||||||
|
const getLog = (needLoading: boolean = true) => {
|
||||||
|
needLoading && setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}public/panel/log`)
|
.get(`${config.apiPrefix}public/panel/log`)
|
||||||
.then((data: any) => {
|
.then(({ code, data }) => {
|
||||||
setData(data.data);
|
if (code === 200) {
|
||||||
|
setData(data);
|
||||||
|
if (!data) {
|
||||||
|
times = getTimes();
|
||||||
|
if (times > 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem('error_retry_times', `${times + 1}`);
|
||||||
|
setTimeout(() => {
|
||||||
|
reloadUser();
|
||||||
|
getLog(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => needLoading && setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -35,7 +56,7 @@ const Error = ({ user, theme }: any) => {
|
|||||||
<div className="error-wrapper">
|
<div className="error-wrapper">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
) : (
|
) : data ? (
|
||||||
<Terminal
|
<Terminal
|
||||||
name="服务错误"
|
name="服务错误"
|
||||||
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
|
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
|
||||||
@@ -51,6 +72,10 @@ const Error = ({ user, theme }: any) => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
) : times > 5 ? (
|
||||||
|
<>服务启动超时,请手动进入容器执行 ql -l check 后刷新再试</>
|
||||||
|
) : (
|
||||||
|
<PageLoading tip="启动中,请稍后..." />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { history } from 'umi';
|
import { history } from '@umijs/max';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
|
|
||||||
@@ -41,11 +41,9 @@ const Initialization = () => {
|
|||||||
password: values.password,
|
password: values.password,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(({ code, data }) => {
|
||||||
if (data.code === 200) {
|
if (code === 200) {
|
||||||
next();
|
next();
|
||||||
} else {
|
|
||||||
message.error(data.message);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
@@ -59,11 +57,9 @@ const Initialization = () => {
|
|||||||
...values,
|
...values,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((_data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (_data && _data.code === 200) {
|
if (code === 200) {
|
||||||
next();
|
next();
|
||||||
} else {
|
|
||||||
message.error(_data.message);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
@@ -116,12 +112,15 @@ const Initialization = () => {
|
|||||||
{config.notificationModes
|
{config.notificationModes
|
||||||
.filter((x) => x.value !== 'closed')
|
.filter((x) => x.value !== 'closed')
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<Option value={x.value}>{x.label}</Option>
|
<Option key={x.value} value={x.value}>
|
||||||
|
{x.label}
|
||||||
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{fields.map((x) => (
|
{fields.map((x) => (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
key={x.label}
|
||||||
label={x.label}
|
label={x.label}
|
||||||
name={x.label}
|
name={x.label}
|
||||||
extra={x.tip}
|
extra={x.tip}
|
||||||
@@ -233,7 +232,7 @@ const Initialization = () => {
|
|||||||
<img
|
<img
|
||||||
alt="logo"
|
alt="logo"
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
src="http://qn.whyour.cn/logo.png"
|
src="https://qn.whyour.cn/logo.png"
|
||||||
/>
|
/>
|
||||||
<span className={styles.title}>初始化配置</span>
|
<span className={styles.title}>初始化配置</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: @component-background;
|
background-color: @component-background;
|
||||||
height: calc(100vh - 128px);
|
height: 100%;
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
+146
-51
@@ -1,5 +1,15 @@
|
|||||||
import { useState, useEffect, useCallback, Key, useRef } from 'react';
|
import { useState, useEffect, useCallback, Key, useRef } from 'react';
|
||||||
import { TreeSelect, Tree, Input, Empty } from 'antd';
|
import {
|
||||||
|
TreeSelect,
|
||||||
|
Tree,
|
||||||
|
Input,
|
||||||
|
Empty,
|
||||||
|
Button,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
@@ -7,53 +17,36 @@ import { request } from '@/utils/http';
|
|||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||||
import SplitPane from 'react-split-pane';
|
import SplitPane from 'react-split-pane';
|
||||||
|
import { useOutletContext } from '@umijs/max';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
|
import { depthFirstSearch } from '@/utils';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import uniq from 'lodash/uniq';
|
||||||
|
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||||
|
|
||||||
function getFilterData(keyword: string, data: any) {
|
const { Text } = Typography;
|
||||||
const expandedKeys: string[] = [];
|
|
||||||
if (keyword) {
|
|
||||||
const tree: any = [];
|
|
||||||
data.forEach((item: any) => {
|
|
||||||
if (item.title.toLocaleLowerCase().includes(keyword)) {
|
|
||||||
tree.push(item);
|
|
||||||
} else {
|
|
||||||
const children: any[] = [];
|
|
||||||
(item.children || []).forEach((subItem: any) => {
|
|
||||||
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
|
|
||||||
children.push(subItem);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (children.length > 0) {
|
|
||||||
tree.push({
|
|
||||||
...item,
|
|
||||||
children,
|
|
||||||
});
|
|
||||||
expandedKeys.push(item.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { tree, expandedKeys };
|
|
||||||
}
|
|
||||||
return { tree: data, expandedKeys };
|
|
||||||
}
|
|
||||||
|
|
||||||
const Log = ({ headerStyle, isPhone, theme }: any) => {
|
const Log = () => {
|
||||||
const [title, setTitle] = useState('请选择日志文件');
|
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||||
const [value, setValue] = useState('请选择日志文件');
|
const [value, setValue] = useState('请选择日志文件');
|
||||||
const [select, setSelect] = useState<any>();
|
const [select, setSelect] = useState<string>('');
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const [filterData, setFilterData] = useState<any[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [height, setHeight] = useState<number>();
|
const [height, setHeight] = useState<number>();
|
||||||
const treeDom = useRef<any>();
|
const treeDom = useRef<any>();
|
||||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||||
|
const [currentNode, setCurrentNode] = useState<any>();
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
const getLogs = () => {
|
const getLogs = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}logs`)
|
.get(`${config.apiPrefix}logs`)
|
||||||
.then((data) => {
|
.then(({ code, data }) => {
|
||||||
setData(data.data);
|
if (code === 200) {
|
||||||
setFilterData(data.data);
|
setData(data);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -61,18 +54,27 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
const getLog = (node: any) => {
|
const getLog = (node: any) => {
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
|
.get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
|
||||||
.then((data) => {
|
.then(({ code, data }) => {
|
||||||
setValue(data.data);
|
if (code === 200) {
|
||||||
|
setValue(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (value: any, node: any) => {
|
const onSelect = (value: any, node: any) => {
|
||||||
|
setCurrentNode(node);
|
||||||
|
setSelect(value);
|
||||||
|
|
||||||
if (node.key === select || !value) {
|
if (node.key === select || !value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.type === 'directory') {
|
||||||
|
setValue('请选择日志文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setValue('加载中...');
|
setValue('加载中...');
|
||||||
setSelect(value);
|
|
||||||
setTitle(node.key);
|
|
||||||
getLog(node);
|
getLog(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,41 +85,130 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
const onSearch = useCallback(
|
const onSearch = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
const keyword = e.target.value;
|
const keyword = e.target.value;
|
||||||
const { tree, expandedKeys } = getFilterData(
|
debounceSearch(keyword);
|
||||||
keyword.toLocaleLowerCase(),
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
setFilterData(tree);
|
|
||||||
setExpandedKeys(expandedKeys);
|
|
||||||
},
|
},
|
||||||
[data, setFilterData],
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const debounceSearch = useCallback(
|
||||||
|
debounce((keyword) => {
|
||||||
|
setSearchValue(keyword);
|
||||||
|
}, 300),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
|
||||||
|
data,
|
||||||
|
searchValue,
|
||||||
|
{ treeNodeFilterProp: 'title' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
|
||||||
|
}, [searchExpandedKeys]);
|
||||||
|
|
||||||
|
const deleteFile = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认删除`,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认删除
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{select}
|
||||||
|
</Text>
|
||||||
|
文件{currentNode.type === 'directory' ? '夹下所以日志' : ''}
|
||||||
|
,删除后不可恢复
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.delete(`${config.apiPrefix}logs`, {
|
||||||
|
data: {
|
||||||
|
filename: currentNode.title,
|
||||||
|
path: currentNode.parent || '',
|
||||||
|
type: currentNode.type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ code }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
message.success(`删除成功`);
|
||||||
|
let newData = [...data];
|
||||||
|
if (currentNode.parent) {
|
||||||
|
newData = depthFirstSearch(
|
||||||
|
newData,
|
||||||
|
(c) => c.key === currentNode.key,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const index = newData.findIndex(
|
||||||
|
(x) => x.key === currentNode.key,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
newData.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setData(newData);
|
||||||
|
initState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initState = () => {
|
||||||
|
setSelect('');
|
||||||
|
setCurrentNode(null);
|
||||||
|
setValue('请选择脚本文件');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExpand = (expKeys: any) => {
|
||||||
|
setExpandedKeys(expKeys);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getLogs();
|
getLogs();
|
||||||
if (treeDom && treeDom.current) {
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (treeDom.current) {
|
||||||
setHeight(treeDom.current.clientHeight);
|
setHeight(treeDom.current.clientHeight);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [treeDom.current, data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="ql-container-wrapper log-wrapper"
|
className="ql-container-wrapper log-wrapper"
|
||||||
title={title}
|
title={select}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
extra={
|
extra={
|
||||||
isPhone && [
|
isPhone
|
||||||
|
? [
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
|
treeExpandAction="click"
|
||||||
className="log-select"
|
className="log-select"
|
||||||
value={select}
|
value={select}
|
||||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||||
treeData={data}
|
treeData={data}
|
||||||
placeholder="请选择日志"
|
placeholder="请选择日志"
|
||||||
fieldNames={{ value: 'key', label: 'title' }}
|
fieldNames={{ value: 'key' }}
|
||||||
|
treeNodeFilterProp="title"
|
||||||
showSearch
|
showSearch
|
||||||
|
allowClear
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>,
|
/>,
|
||||||
]
|
]
|
||||||
|
: [
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={!select}
|
||||||
|
onClick={deleteFile}
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
header={{
|
header={{
|
||||||
style: headerStyle,
|
style: headerStyle,
|
||||||
@@ -125,6 +216,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
>
|
>
|
||||||
<div className={`${styles['log-container']} log-container`}>
|
<div className={`${styles['log-container']} log-container`}>
|
||||||
{!isPhone && (
|
{!isPhone && (
|
||||||
|
/*// @ts-ignore*/
|
||||||
<SplitPane split="vertical" size={200} maxSize={-100}>
|
<SplitPane split="vertical" size={200} maxSize={-100}>
|
||||||
<div className={styles['left-tree-container']}>
|
<div className={styles['left-tree-container']}>
|
||||||
{data.length > 0 ? (
|
{data.length > 0 ? (
|
||||||
@@ -137,6 +229,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
></Input.Search>
|
></Input.Search>
|
||||||
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
||||||
<Tree
|
<Tree
|
||||||
|
expandAction="click"
|
||||||
className={styles['left-tree']}
|
className={styles['left-tree']}
|
||||||
treeData={filterData}
|
treeData={filterData}
|
||||||
showIcon={true}
|
showIcon={true}
|
||||||
@@ -144,6 +237,8 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
|||||||
selectedKeys={[select]}
|
selectedKeys={[select]}
|
||||||
showLine={{ showLeafIcon: true }}
|
showLine={{ showLeafIcon: true }}
|
||||||
onSelect={onTreeSelect}
|
onSelect={onTreeSelect}
|
||||||
|
expandedKeys={expandedKeys}
|
||||||
|
onExpand={onExpand}
|
||||||
></Tree>
|
></Tree>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+20
-26
@@ -9,16 +9,18 @@ import {
|
|||||||
Statistic,
|
Statistic,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { history, Link } from 'umi';
|
import { history, useOutletContext } from '@umijs/max';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import { useTheme } from '@/utils/hooks';
|
import { useTheme } from '@/utils/hooks';
|
||||||
import { MobileOutlined } from '@ant-design/icons';
|
import { MobileOutlined } from '@ant-design/icons';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
|
||||||
const FormItem = Form.Item;
|
const FormItem = Form.Item;
|
||||||
const { Countdown } = Statistic;
|
const { Countdown } = Statistic;
|
||||||
|
|
||||||
const Login = ({ reloadUser }: any) => {
|
const Login = () => {
|
||||||
|
const { reloadUser } = useOutletContext<SharedContext>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [waitTime, setWaitTime] = useState<any>();
|
const [waitTime, setWaitTime] = useState<any>();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -38,15 +40,7 @@ const Login = ({ reloadUser }: any) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.code === 420) {
|
checkResponse(data, values);
|
||||||
setLoginInfo({
|
|
||||||
username: values.username,
|
|
||||||
password: values.password,
|
|
||||||
});
|
|
||||||
setTwoFactor(true);
|
|
||||||
} else {
|
|
||||||
checkResponse(data);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
@@ -62,11 +56,7 @@ const Login = ({ reloadUser }: any) => {
|
|||||||
data: { ...loginInfo, code: values.code },
|
data: { ...loginInfo, code: values.code },
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data.code === 430) {
|
|
||||||
message.error(data.message);
|
|
||||||
} else {
|
|
||||||
checkResponse(data);
|
checkResponse(data);
|
||||||
}
|
|
||||||
setVerifying(false);
|
setVerifying(false);
|
||||||
})
|
})
|
||||||
.catch((error: any) => {
|
.catch((error: any) => {
|
||||||
@@ -75,8 +65,11 @@ const Login = ({ reloadUser }: any) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkResponse = (data: any) => {
|
const checkResponse = (
|
||||||
if (data.code === 200) {
|
{ code, data, message: _message }: any,
|
||||||
|
values?: any,
|
||||||
|
) => {
|
||||||
|
if (code === 200) {
|
||||||
const {
|
const {
|
||||||
token,
|
token,
|
||||||
lastip,
|
lastip,
|
||||||
@@ -84,7 +77,7 @@ const Login = ({ reloadUser }: any) => {
|
|||||||
lastlogon,
|
lastlogon,
|
||||||
retries = 0,
|
retries = 0,
|
||||||
platform,
|
platform,
|
||||||
} = data.data;
|
} = data;
|
||||||
localStorage.setItem(config.authKey, token);
|
localStorage.setItem(config.authKey, token);
|
||||||
notification.success({
|
notification.success({
|
||||||
message: '登录成功!',
|
message: '登录成功!',
|
||||||
@@ -103,13 +96,14 @@ const Login = ({ reloadUser }: any) => {
|
|||||||
});
|
});
|
||||||
reloadUser(true);
|
reloadUser(true);
|
||||||
history.push('/crontab');
|
history.push('/crontab');
|
||||||
} else if (data.code === 100) {
|
} else if (code === 410) {
|
||||||
message.warn(data.message);
|
setWaitTime(data);
|
||||||
} else if (data.code === 410) {
|
} else if (code === 420) {
|
||||||
message.error(data.message);
|
setLoginInfo({
|
||||||
setWaitTime(data.data);
|
username: values.username,
|
||||||
} else {
|
password: values.password,
|
||||||
message.error(data.message);
|
});
|
||||||
|
setTwoFactor(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +129,7 @@ const Login = ({ reloadUser }: any) => {
|
|||||||
<img
|
<img
|
||||||
alt="logo"
|
alt="logo"
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
src="http://qn.whyour.cn/logo.png"
|
src="https://qn.whyour.cn/logo.png"
|
||||||
/>
|
/>
|
||||||
<span className={styles.title}>
|
<span className={styles.title}>
|
||||||
{twoFactor ? '两步验证' : config.siteName}
|
{twoFactor ? '两步验证' : config.siteName}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Editor from '@monaco-editor/react';
|
|||||||
import SaveModal from './saveModal';
|
import SaveModal from './saveModal';
|
||||||
import SettingModal from './setting';
|
import SettingModal from './setting';
|
||||||
import { useTheme } from '@/utils/hooks';
|
import { useTheme } from '@/utils/hooks';
|
||||||
|
import { logEnded } from '@/utils';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const LangMap: any = {
|
const LangMap: any = {
|
||||||
@@ -48,6 +49,7 @@ const EditModal = ({
|
|||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
const [currentPid, setCurrentPid] = useState(null);
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
handleCancel();
|
handleCancel();
|
||||||
@@ -57,6 +59,11 @@ const EditModal = ({
|
|||||||
if (node.key === selectedKey || !value) {
|
if (node.key === selectedKey || !value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.type === 'directory') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newMode = LangMap[value.slice(-3)] || '';
|
const newMode = LangMap[value.slice(-3)] || '';
|
||||||
setCNode(node);
|
setCNode(node);
|
||||||
setLanguage(newMode);
|
setLanguage(newMode);
|
||||||
@@ -67,8 +74,10 @@ const EditModal = ({
|
|||||||
const getDetail = (node: any) => {
|
const getDetail = (node: any) => {
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
|
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
|
||||||
.then((data) => {
|
.then(({ code, data }) => {
|
||||||
setValue(data.data);
|
if (code === 200) {
|
||||||
|
setValue(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,26 +92,30 @@ const EditModal = ({
|
|||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
|
setCurrentPid(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
if (!cNode || !cNode.title) {
|
if (!cNode || !cNode.title || !currentPid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const content = editorRef.current.getValue().replace(/\r\n/g, '\n');
|
|
||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}scripts/stop`, {
|
.put(`${config.apiPrefix}scripts/stop`, {
|
||||||
data: {
|
data: {
|
||||||
filename: cNode.title,
|
filename: cNode.title,
|
||||||
path: cNode.parent || '',
|
path: cNode.parent || '',
|
||||||
content,
|
pid: currentPid,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,7 +130,7 @@ const EditModal = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_message.includes('执行结束')) {
|
if (logEnded(_message)) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
}, 300);
|
}, 300);
|
||||||
@@ -130,10 +143,13 @@ const EditModal = ({
|
|||||||
}, [socketMessage]);
|
}, [socketMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setLog('');
|
||||||
if (currentNode) {
|
if (currentNode) {
|
||||||
setCNode(currentNode);
|
setCNode(currentNode);
|
||||||
setValue(content as string);
|
setValue(content as string);
|
||||||
setSelectedKey(currentNode.key);
|
setSelectedKey(currentNode.key);
|
||||||
|
const newMode = LangMap[currentNode.title.slice(-3)] || '';
|
||||||
|
setLanguage(newMode);
|
||||||
}
|
}
|
||||||
}, [content, currentNode]);
|
}, [content, currentNode]);
|
||||||
|
|
||||||
@@ -144,6 +160,7 @@ const EditModal = ({
|
|||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
|
treeExpandAction="click"
|
||||||
style={{ marginRight: 8, width: 150 }}
|
style={{ marginRight: 8, width: 150 }}
|
||||||
value={selectedKey}
|
value={selectedKey}
|
||||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||||
@@ -214,8 +231,9 @@ const EditModal = ({
|
|||||||
width={'100%'}
|
width={'100%'}
|
||||||
headerStyle={{ padding: '11px 24px' }}
|
headerStyle={{ padding: '11px 24px' }}
|
||||||
onClose={cancel}
|
onClose={cancel}
|
||||||
visible={visible}
|
open={visible}
|
||||||
>
|
>
|
||||||
|
{/* @ts-ignore */}
|
||||||
<SplitPane
|
<SplitPane
|
||||||
split="vertical"
|
split="vertical"
|
||||||
minSize={200}
|
minSize={200}
|
||||||
@@ -236,7 +254,14 @@ const EditModal = ({
|
|||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre>
|
<pre
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
padding: '0 15px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{log}
|
||||||
|
</pre>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
<SaveModal
|
<SaveModal
|
||||||
visible={saveModalVisible}
|
visible={saveModalVisible}
|
||||||
@@ -247,7 +272,7 @@ const EditModal = ({
|
|||||||
content:
|
content:
|
||||||
editorRef.current &&
|
editorRef.current &&
|
||||||
editorRef.current.getValue().replace(/\r\n/g, '\n'),
|
editorRef.current.getValue().replace(/\r\n/g, '\n'),
|
||||||
filename: cNode?.title,
|
...cNode,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SettingModal
|
<SettingModal
|
||||||
|
|||||||
@@ -32,32 +32,31 @@ const EditScriptNameModal = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [dirs, setDirs] = useState<any[]>([]);
|
const [dirs, setDirs] = useState<any[]>([]);
|
||||||
const [file, setFile] = useState<File>();
|
const [file, setFile] = useState<File>();
|
||||||
const [type, setType] = useState<'blank' | 'upload'>('blank');
|
const [type, setType] = useState<'blank' | 'upload' | 'directory'>('blank');
|
||||||
|
|
||||||
const handleOk = async (values: any) => {
|
const handleOk = async (values: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
values.path = values.path || '';
|
const { path = '', filename: inputFilename, directory = '' } = values;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file as any);
|
formData.append('file', file as any);
|
||||||
formData.append('filename', values.filename);
|
formData.append('filename', inputFilename);
|
||||||
formData.append('path', values.path);
|
formData.append('path', path);
|
||||||
formData.append('content', '');
|
formData.append('content', '');
|
||||||
|
formData.append('directory', directory);
|
||||||
request
|
request
|
||||||
.post(`${config.apiPrefix}scripts`, {
|
.post(`${config.apiPrefix}scripts`, {
|
||||||
data: formData,
|
data: formData,
|
||||||
})
|
})
|
||||||
.then(({ code, data }) => {
|
.then(({ code, data }) => {
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
message.success('新建文件成功');
|
message.success(directory ? '新建文件夹成功' : '新建文件成功');
|
||||||
const key = values.path ? `${values.path}/` : '';
|
const key = path ? `${path}/` : '';
|
||||||
const filename = file ? file.name : values.filename;
|
const filename = file ? file.name : inputFilename;
|
||||||
handleCancel({
|
handleCancel({
|
||||||
filename,
|
filename,
|
||||||
path: values.path,
|
path,
|
||||||
key: `${key}${filename}`,
|
key: `${key}${filename}`,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
message.error(data);
|
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
@@ -99,8 +98,8 @@ const EditScriptNameModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="新建脚本"
|
title="新建"
|
||||||
visible={visible}
|
open={visible}
|
||||||
forceRender
|
forceRender
|
||||||
centered
|
centered
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
@@ -126,18 +125,36 @@ const EditScriptNameModal = ({
|
|||||||
>
|
>
|
||||||
<Radio.Group onChange={typeChange}>
|
<Radio.Group onChange={typeChange}>
|
||||||
<Radio value="blank">空文件</Radio>
|
<Radio value="blank">空文件</Radio>
|
||||||
<Radio value="upload">本地上传</Radio>
|
<Radio value="upload">本地文件</Radio>
|
||||||
|
<Radio value="directory">文件夹</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{type === 'blank' && (
|
{type === 'blank' && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="filename"
|
name="filename"
|
||||||
label="文件名"
|
label="文件名"
|
||||||
rules={[{ required: true, message: '请输入文件名' }]}
|
rules={[
|
||||||
|
{ required: true, message: '请输入文件名' },
|
||||||
|
{
|
||||||
|
validator: (_, value) =>
|
||||||
|
value.includes('/')
|
||||||
|
? Promise.reject(new Error('文件名不能包含斜杠'))
|
||||||
|
: Promise.resolve(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入文件名" />
|
<Input placeholder="请输入文件名" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
{type === 'directory' && (
|
||||||
|
<Form.Item
|
||||||
|
name="directory"
|
||||||
|
label="文件夹名"
|
||||||
|
rules={[{ required: true, message: '请输入文件夹名' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入文件夹名" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
<Form.Item label="父目录" name="path">
|
<Form.Item label="父目录" name="path">
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
allowClear
|
allowClear
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: @component-background;
|
background-color: @component-background;
|
||||||
height: calc(100vh - 128px);
|
height: 100%;
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
+150
-124
@@ -11,6 +11,7 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
Menu,
|
Menu,
|
||||||
Empty,
|
Empty,
|
||||||
|
MenuProps,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
@@ -33,38 +34,17 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import EditScriptNameModal from './editNameModal';
|
import EditScriptNameModal from './editNameModal';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { history } from 'umi';
|
import { history, useOutletContext, useLocation } from '@umijs/max';
|
||||||
|
import { parse } from 'query-string';
|
||||||
|
import { depthFirstSearch, findNode } from '@/utils';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||||
|
import uniq from 'lodash/uniq';
|
||||||
|
import IconFont from '@/components/iconfont';
|
||||||
|
import RenameModal from './renameModal';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
function getFilterData(keyword: string, data: any) {
|
|
||||||
const expandedKeys: string[] = [];
|
|
||||||
if (keyword) {
|
|
||||||
const tree: any = [];
|
|
||||||
data.forEach((item: any) => {
|
|
||||||
if (item.title.toLocaleLowerCase().includes(keyword)) {
|
|
||||||
tree.push(item);
|
|
||||||
} else {
|
|
||||||
const children: any[] = [];
|
|
||||||
(item.children || []).forEach((subItem: any) => {
|
|
||||||
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
|
|
||||||
children.push(subItem);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (children.length > 0) {
|
|
||||||
tree.push({
|
|
||||||
...item,
|
|
||||||
children,
|
|
||||||
});
|
|
||||||
expandedKeys.push(item.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { tree, expandedKeys };
|
|
||||||
}
|
|
||||||
return { tree: data, expandedKeys };
|
|
||||||
}
|
|
||||||
|
|
||||||
const LangMap: any = {
|
const LangMap: any = {
|
||||||
'.py': 'python',
|
'.py': 'python',
|
||||||
'.js': 'javascript',
|
'.js': 'javascript',
|
||||||
@@ -72,12 +52,12 @@ const LangMap: any = {
|
|||||||
'.ts': 'typescript',
|
'.ts': 'typescript',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
const Script = () => {
|
||||||
const [title, setTitle] = useState('请选择脚本文件');
|
const { headerStyle, isPhone, theme, socketMessage } =
|
||||||
|
useOutletContext<SharedContext>();
|
||||||
const [value, setValue] = useState('请选择脚本文件');
|
const [value, setValue] = useState('请选择脚本文件');
|
||||||
const [select, setSelect] = useState<any>();
|
const [select, setSelect] = useState<string>('');
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const [filterData, setFilterData] = useState<any[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [mode, setMode] = useState('');
|
const [mode, setMode] = useState('');
|
||||||
const [height, setHeight] = useState<number>();
|
const [height, setHeight] = useState<number>();
|
||||||
@@ -87,31 +67,41 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
|
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
|
||||||
|
const [isRenameFileModalVisible, setIsRenameFileModalVisible] =
|
||||||
|
useState(false);
|
||||||
const [currentNode, setCurrentNode] = useState<any>();
|
const [currentNode, setCurrentNode] = useState<any>();
|
||||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
const getScripts = () => {
|
const getScripts = (needLoading: boolean = true) => {
|
||||||
setLoading(true);
|
needLoading && setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}scripts`)
|
.get(`${config.apiPrefix}scripts`)
|
||||||
.then((data) => {
|
.then(({ code, data }) => {
|
||||||
setData(data.data);
|
if (code === 200) {
|
||||||
setFilterData(data.data);
|
setData(data);
|
||||||
initGetScript();
|
initState();
|
||||||
|
initGetScript(data);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => needLoading && setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDetail = (node: any) => {
|
const getDetail = (node: any) => {
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
|
.get(
|
||||||
.then((data) => {
|
`${config.apiPrefix}scripts/${encodeURIComponent(node.title)}?path=${
|
||||||
setValue(data.data);
|
node.parent || ''
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
setValue(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const initGetScript = () => {
|
const initGetScript = (_data: any) => {
|
||||||
const { p, s } = history.location.query as any;
|
const { p, s } = parse(history.location.search);
|
||||||
if (s) {
|
if (s) {
|
||||||
const vkey = `${p}/${s}`;
|
const vkey = `${p}/${s}`;
|
||||||
const obj = {
|
const obj = {
|
||||||
@@ -121,28 +111,33 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
parent: p,
|
parent: p,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
setExpandedKeys([p]);
|
const item = findNode(_data, (c) => c.key === obj.node.key);
|
||||||
|
if (item) {
|
||||||
|
setExpandedKeys([p as string]);
|
||||||
onTreeSelect([vkey], obj);
|
onTreeSelect([vkey], obj);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (value: any, node: any) => {
|
const onSelect = (value: any, node: any) => {
|
||||||
|
setSelect(node.key);
|
||||||
|
setCurrentNode(node);
|
||||||
|
|
||||||
if (node.key === select || !value) {
|
if (node.key === select || !value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setValue('加载中...');
|
|
||||||
|
if (node.type === 'directory') {
|
||||||
|
setValue('请选择脚本文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newMode = value ? LangMap[value.slice(-3)] : '';
|
const newMode = value ? LangMap[value.slice(-3)] : '';
|
||||||
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
|
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
|
||||||
setSelect(node.key);
|
setValue('加载中...');
|
||||||
setTitle(node.key);
|
|
||||||
setCurrentNode(node);
|
|
||||||
getDetail(node);
|
getDetail(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onExpand = (expKeys: any) => {
|
|
||||||
setExpandedKeys(expKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTreeSelect = useCallback(
|
const onTreeSelect = useCallback(
|
||||||
(keys: Key[], e: any) => {
|
(keys: Key[], e: any) => {
|
||||||
const content = editorRef.current
|
const content = editorRef.current
|
||||||
@@ -173,22 +168,30 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
const keyword = e.target.value;
|
const keyword = e.target.value;
|
||||||
debounceSearch(keyword);
|
debounceSearch(keyword);
|
||||||
},
|
},
|
||||||
[data, setFilterData],
|
[data],
|
||||||
);
|
);
|
||||||
|
|
||||||
const debounceSearch = useCallback(
|
const debounceSearch = useCallback(
|
||||||
debounce((keyword) => {
|
debounce((keyword) => {
|
||||||
setSearchValue(keyword);
|
setSearchValue(keyword);
|
||||||
const { tree, expandedKeys } = getFilterData(
|
|
||||||
keyword.toLocaleLowerCase(),
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
setExpandedKeys(expandedKeys);
|
|
||||||
setFilterData(tree);
|
|
||||||
}, 300),
|
}, 300),
|
||||||
[data, setFilterData],
|
[data],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
|
||||||
|
data,
|
||||||
|
searchValue,
|
||||||
|
{ treeNodeFilterProp: 'title' },
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
|
||||||
|
}, [searchExpandedKeys]);
|
||||||
|
|
||||||
|
const onExpand = (expKeys: any) => {
|
||||||
|
setExpandedKeys(expKeys);
|
||||||
|
};
|
||||||
|
|
||||||
const editFile = () => {
|
const editFile = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
@@ -226,13 +229,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((_data: any) => {
|
.then(({ code, data }) => {
|
||||||
if (_data.code === 200) {
|
if (code === 200) {
|
||||||
message.success(`保存成功`);
|
message.success(`保存成功`);
|
||||||
setValue(content);
|
setValue(content);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
} else {
|
|
||||||
message.error(_data);
|
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
})
|
})
|
||||||
@@ -250,10 +251,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
title: `确认删除`,
|
title: `确认删除`,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
确认删除文件
|
确认删除
|
||||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
{select}
|
{select}
|
||||||
</Text>{' '}
|
</Text>
|
||||||
|
文件{currentNode.type === 'directory' ? '夹及其子文件' : ''}
|
||||||
,删除后不可恢复
|
,删除后不可恢复
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -263,24 +265,18 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
data: {
|
data: {
|
||||||
filename: currentNode.title,
|
filename: currentNode.title,
|
||||||
path: currentNode.parent || '',
|
path: currentNode.parent || '',
|
||||||
|
type: currentNode.type,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((_data: any) => {
|
.then(({ code }) => {
|
||||||
if (_data.code === 200) {
|
if (code === 200) {
|
||||||
message.success(`删除成功`);
|
message.success(`删除成功`);
|
||||||
let newData = [...data];
|
let newData = [...data];
|
||||||
if (currentNode.parent) {
|
if (currentNode.parent) {
|
||||||
const parentNodeIndex = newData.findIndex(
|
newData = depthFirstSearch(
|
||||||
(x) => x.key === currentNode.parent,
|
newData,
|
||||||
|
(c) => c.key === currentNode.key,
|
||||||
);
|
);
|
||||||
const parentNode = newData[parentNodeIndex];
|
|
||||||
const index = parentNode.children.findIndex(
|
|
||||||
(y) => y.key === currentNode.key,
|
|
||||||
);
|
|
||||||
if (index !== -1 && parentNodeIndex !== -1) {
|
|
||||||
parentNode.children.splice(index, 1);
|
|
||||||
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const index = newData.findIndex(
|
const index = newData.findIndex(
|
||||||
(x) => x.key === currentNode.key,
|
(x) => x.key === currentNode.key,
|
||||||
@@ -290,8 +286,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setData(newData);
|
setData(newData);
|
||||||
} else {
|
initState();
|
||||||
message.error(_data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -301,6 +296,15 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renameFile = () => {
|
||||||
|
setIsRenameFileModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenameFileCancel = () => {
|
||||||
|
setIsRenameFileModalVisible(false);
|
||||||
|
getScripts(false);
|
||||||
|
};
|
||||||
|
|
||||||
const addFile = () => {
|
const addFile = () => {
|
||||||
setIsAddFileModalVisible(true);
|
setIsAddFileModalVisible(true);
|
||||||
};
|
};
|
||||||
@@ -313,20 +317,17 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (filename) {
|
if (filename) {
|
||||||
const newData = [...data];
|
let newData = [...data];
|
||||||
const _file = { title: filename, key, parent: path };
|
const _file = { title: filename, key, parent: path };
|
||||||
if (path) {
|
if (path) {
|
||||||
// TODO: 更新左侧树数据
|
newData = depthFirstSearch(newData, (c) => c.key === path, _file);
|
||||||
const parentNodeIndex = newData.findIndex((x) => x.key === path);
|
const keys = path.split('/');
|
||||||
if (parentNodeIndex !== -1) {
|
const sKeys: string[] = [];
|
||||||
const parentNode = newData[parentNodeIndex];
|
keys.reduce((p, c) => {
|
||||||
if (parentNode.children && parentNode.children.length > 0) {
|
sKeys.push(p);
|
||||||
parentNode.children.unshift(_file);
|
return `${p}/${c}`;
|
||||||
} else {
|
});
|
||||||
parentNode.children = [_file];
|
setExpandedKeys([...expandedKeys, ...sKeys, path]);
|
||||||
}
|
|
||||||
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
newData.unshift(_file);
|
newData.unshift(_file);
|
||||||
}
|
}
|
||||||
@@ -344,8 +345,9 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
filename: currentNode.title,
|
filename: currentNode.title,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((_data: any) => {
|
.then(({ code, data }) => {
|
||||||
const blob = new Blob([_data], { type: 'application/json' });
|
if (code === 200) {
|
||||||
|
const blob = new Blob([data], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
@@ -353,25 +355,25 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
document.documentElement.appendChild(a);
|
document.documentElement.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.documentElement.removeChild(a);
|
document.documentElement.removeChild(a);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const initState = () => {
|
||||||
const word = searchValue || '';
|
|
||||||
const { tree } = getFilterData(word.toLocaleLowerCase(), data);
|
|
||||||
setFilterData(tree);
|
|
||||||
setSelect('');
|
setSelect('');
|
||||||
setCurrentNode(null);
|
setCurrentNode(null);
|
||||||
setTitle('请选择脚本文件');
|
|
||||||
setValue('请选择脚本文件');
|
setValue('请选择脚本文件');
|
||||||
}, [data]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getScripts();
|
getScripts();
|
||||||
if (treeDom && treeDom.current) {
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (treeDom.current) {
|
||||||
setHeight(treeDom.current.clientHeight);
|
setHeight(treeDom.current.clientHeight);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [treeDom.current, data]);
|
||||||
|
|
||||||
const action = (key: string | number) => {
|
const action = (key: string | number) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
@@ -388,7 +390,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
|
|
||||||
const menuAction = (key: string | number) => {
|
const menuAction = (key: string | number) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'save':
|
case 'add':
|
||||||
addFile();
|
addFile();
|
||||||
break;
|
break;
|
||||||
case 'edit':
|
case 'edit':
|
||||||
@@ -397,25 +399,27 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
case 'delete':
|
case 'delete':
|
||||||
deleteFile();
|
deleteFile();
|
||||||
break;
|
break;
|
||||||
|
case 'rename':
|
||||||
|
renameFile();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu = isEditing ? (
|
const menu: MenuProps = isEditing
|
||||||
<Menu
|
? {
|
||||||
items={[
|
items: [
|
||||||
{ label: '保存', key: 'save', icon: <PlusOutlined /> },
|
{ label: '保存', key: 'save', icon: <PlusOutlined /> },
|
||||||
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
|
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
|
||||||
]}
|
],
|
||||||
onClick={({ key, domEvent }) => {
|
onClick: ({ key, domEvent }) => {
|
||||||
domEvent.stopPropagation();
|
domEvent.stopPropagation();
|
||||||
action(key);
|
action(key);
|
||||||
}}
|
},
|
||||||
/>
|
}
|
||||||
) : (
|
: {
|
||||||
<Menu
|
items: [
|
||||||
items={[
|
|
||||||
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
|
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
@@ -423,39 +427,47 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
icon: <EditOutlined />,
|
icon: <EditOutlined />,
|
||||||
disabled: !select,
|
disabled: !select,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '重命名',
|
||||||
|
key: 'rename',
|
||||||
|
icon: <IconFont type="ql-icon-rename" />,
|
||||||
|
disabled: !select,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: '删除',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined />,
|
||||||
disabled: !select,
|
disabled: !select,
|
||||||
},
|
},
|
||||||
]}
|
],
|
||||||
onClick={({ key, domEvent }) => {
|
onClick: ({ key, domEvent }) => {
|
||||||
domEvent.stopPropagation();
|
domEvent.stopPropagation();
|
||||||
menuAction(key);
|
menuAction(key);
|
||||||
}}
|
},
|
||||||
/>
|
};
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="ql-container-wrapper log-wrapper"
|
className="ql-container-wrapper log-wrapper"
|
||||||
title={title}
|
title={select}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
extra={
|
extra={
|
||||||
isPhone
|
isPhone
|
||||||
? [
|
? [
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
|
treeExpandAction="click"
|
||||||
className="log-select"
|
className="log-select"
|
||||||
value={select}
|
value={select}
|
||||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||||
treeData={data}
|
treeData={data}
|
||||||
placeholder="请选择脚本"
|
placeholder="请选择脚本"
|
||||||
fieldNames={{ value: 'key', label: 'title' }}
|
fieldNames={{ value: 'key' }}
|
||||||
|
treeNodeFilterProp="title"
|
||||||
showSearch
|
showSearch
|
||||||
|
allowClear
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>,
|
/>,
|
||||||
<Dropdown overlay={menu} trigger={['click']}>
|
<Dropdown menu={menu} trigger={['click']}>
|
||||||
<Button type="primary" icon={<EllipsisOutlined />} />
|
<Button type="primary" icon={<EllipsisOutlined />} />
|
||||||
</Dropdown>,
|
</Dropdown>,
|
||||||
]
|
]
|
||||||
@@ -484,6 +496,14 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>,
|
</Tooltip>,
|
||||||
|
<Tooltip title="重命名">
|
||||||
|
<Button
|
||||||
|
disabled={!select}
|
||||||
|
type="primary"
|
||||||
|
onClick={renameFile}
|
||||||
|
icon={<IconFont type="ql-icon-rename" />}
|
||||||
|
/>
|
||||||
|
</Tooltip>,
|
||||||
<Tooltip title="删除">
|
<Tooltip title="删除">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -508,6 +528,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
>
|
>
|
||||||
<div className={`${styles['log-container']} log-container`}>
|
<div className={`${styles['log-container']} log-container`}>
|
||||||
{!isPhone && (
|
{!isPhone && (
|
||||||
|
/*// @ts-ignore*/
|
||||||
<SplitPane split="vertical" size={200} maxSize={-100}>
|
<SplitPane split="vertical" size={200} maxSize={-100}>
|
||||||
<div className={styles['left-tree-container']}>
|
<div className={styles['left-tree-container']}>
|
||||||
{data.length > 0 ? (
|
{data.length > 0 ? (
|
||||||
@@ -520,6 +541,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
></Input.Search>
|
></Input.Search>
|
||||||
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
||||||
<Tree
|
<Tree
|
||||||
|
expandAction="click"
|
||||||
className={styles['left-tree']}
|
className={styles['left-tree']}
|
||||||
treeData={filterData}
|
treeData={filterData}
|
||||||
showIcon={true}
|
showIcon={true}
|
||||||
@@ -556,7 +578,6 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
readOnly: !isEditing,
|
readOnly: !isEditing,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
folding: false,
|
|
||||||
glyphMargin: false,
|
glyphMargin: false,
|
||||||
}}
|
}}
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
@@ -597,6 +618,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
|||||||
treeData={data}
|
treeData={data}
|
||||||
handleCancel={addFileModalClose}
|
handleCancel={addFileModalClose}
|
||||||
/>
|
/>
|
||||||
|
<RenameModal
|
||||||
|
visible={isRenameFileModalVisible}
|
||||||
|
handleCancel={handleRenameFileCancel}
|
||||||
|
currentNode={currentNode}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, message, Input, Form } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
|
||||||
|
const RenameModal = ({
|
||||||
|
currentNode,
|
||||||
|
handleCancel,
|
||||||
|
visible,
|
||||||
|
}: {
|
||||||
|
currentNode?: any;
|
||||||
|
visible: boolean;
|
||||||
|
handleCancel: () => void;
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleOk = async (values: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { code, data } = await request.put(
|
||||||
|
`${config.apiPrefix}scripts/rename`,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
filename: currentNode.title,
|
||||||
|
path: currentNode.parent || '',
|
||||||
|
newFilename: values.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (code === 200) {
|
||||||
|
message.success('更新名称成功');
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.resetFields();
|
||||||
|
}, [currentNode, visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="重命名"
|
||||||
|
open={visible}
|
||||||
|
forceRender
|
||||||
|
centered
|
||||||
|
maskClosable={false}
|
||||||
|
onOk={() => {
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then((values) => {
|
||||||
|
handleOk(values);
|
||||||
|
})
|
||||||
|
.catch((info) => {
|
||||||
|
console.log('Validate Failed:', info);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => handleCancel()}
|
||||||
|
confirmLoading={loading}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" name="edit_name_modal">
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: '请输入新名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入新名称" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RenameModal;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user