mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 19:30:48 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db227e56bf | ||
|
|
84828865f7 | ||
|
|
87455cebc0 | ||
|
|
9f4435b237 | ||
|
|
e5bc4b3b04 | ||
|
|
11e1b50000 | ||
|
|
bec226ae13 | ||
|
|
c9ecdc6c97 | ||
|
|
4cf64e7af0 | ||
|
|
4ce01c5f0e | ||
|
|
2eff99abe1 | ||
|
|
5bf57185d9 | ||
|
|
c59dac19ae | ||
|
|
f1ef8e72ae | ||
|
|
5b1accffb7 | ||
|
|
f64ff83414 | ||
|
|
26c914d1db | ||
|
|
8f75a54110 | ||
|
|
874b6e3a56 | ||
|
|
f305d45225 | ||
|
|
a7f97bd729 | ||
|
|
0bbb7c7b89 | ||
|
|
f7465759c7 | ||
|
|
998e82d323 | ||
|
|
f8dfee8945 | ||
|
|
86e3d8736b | ||
|
|
85d32d00e1 | ||
|
|
6a52e2f804 | ||
|
|
b4cb72c5d0 | ||
|
|
79fd4ec58f | ||
|
|
49ed5c6632 | ||
|
|
ffba7ee4a1 | ||
|
|
91d080472f | ||
|
|
826f214f0f | ||
|
|
ca150dc6a6 | ||
|
|
ae5db60211 | ||
|
|
490bdc15f6 | ||
|
|
1377fe31aa | ||
|
|
3d1cf6bf2e | ||
|
|
6ac755b478 | ||
|
|
e70fa83074 | ||
|
|
41521f1d08 | ||
|
|
8b7bef097c | ||
|
|
618756d4a9 | ||
|
|
70d0c71414 | ||
|
|
65eb9c806d | ||
|
|
50718ba714 | ||
|
|
7c3bb90d12 | ||
|
|
f66390eabd | ||
|
|
481646501c | ||
|
|
5baa8e4391 | ||
|
|
3d11210aeb | ||
|
|
e1863407f2 | ||
|
|
0c28b63ab7 | ||
|
|
8b8bd279c6 | ||
|
|
01b404765e | ||
|
|
c231a871d7 | ||
|
|
598c6b4e57 | ||
|
|
2295fabcc5 | ||
|
|
be94d796ac | ||
|
|
c84908d7fa | ||
|
|
d8ae039b92 | ||
|
|
bce1431d03 | ||
|
|
9db0095e29 | ||
|
|
77dc7817fb | ||
|
|
d0dd97631e | ||
|
|
d11d6d0c18 | ||
|
|
0af687f781 | ||
|
|
8db997abe8 | ||
|
|
aab6bbeb15 | ||
|
|
df1addc1ff | ||
|
|
1f8f35476a | ||
|
|
5354fc76db | ||
|
|
b29a9e012c | ||
|
|
6e5d89c197 | ||
|
|
d6cfb18f06 | ||
|
|
9a3e38051d | ||
|
|
b705ad6ee8 | ||
|
|
db564d1add | ||
|
|
4f422c0658 | ||
|
|
f267720af4 | ||
|
|
4a0c66bcc0 | ||
|
|
ef7283a9fd | ||
|
|
4498449eea | ||
|
|
ebb9676a51 | ||
|
|
6fb39ce835 | ||
|
|
59c26d90d3 | ||
|
|
1d2df860e8 | ||
|
|
ce22cad5b2 | ||
|
|
dc44ce6b1a | ||
|
|
b27ee23cc3 | ||
|
|
083c8869aa | ||
|
|
6a971a0d6e | ||
|
|
a25bfb6912 | ||
|
|
f19dd21155 | ||
|
|
79b342d1d2 | ||
|
|
20b0d75b0d | ||
|
|
c89724906e | ||
|
|
c3a548ece3 | ||
|
|
b431e39f8f | ||
|
|
710746809b | ||
|
|
8a225e9beb | ||
|
|
a5ecb204b2 | ||
|
|
f1f009da3b | ||
|
|
7291098f98 | ||
|
|
666545ff03 | ||
|
|
81e2bbb14c | ||
|
|
970dff144c | ||
|
|
819b15b15e | ||
|
|
3fa57c69ac | ||
|
|
18689c8119 | ||
|
|
61e7049baa | ||
|
|
39c40a328d | ||
|
|
955d815d14 | ||
|
|
3a3b945de8 | ||
|
|
73c53dbc8d | ||
|
|
f76828d4ec | ||
|
|
7b2956eb0a | ||
|
|
b2167ae5e4 | ||
|
|
98dfa4c425 | ||
|
|
5bf08b76de | ||
|
|
5c5b4f804e | ||
|
|
6455245d11 | ||
|
|
2ff3d9c601 | ||
|
|
d342fbf29f | ||
|
|
f865018cd6 | ||
|
|
3b9a4f0834 | ||
|
|
67bc305950 | ||
|
|
97f128d59f | ||
|
|
e25885c4d9 | ||
|
|
c285026339 | ||
|
|
6c34045a48 | ||
|
|
e1e6261b4f | ||
|
|
697bcb5922 | ||
|
|
e05b0a2491 | ||
|
|
e1655455d8 | ||
|
|
9921dab064 | ||
|
|
403ab7f0f0 | ||
|
|
9bc7ca4094 | ||
|
|
e924aa76b0 | ||
|
|
0c46606e14 | ||
|
|
dbfa4049f5 | ||
|
|
622fe2a8f8 | ||
|
|
7bce5c4f6a | ||
|
|
1f7f2c8971 | ||
|
|
437a849c2a | ||
|
|
8f7261dd50 | ||
|
|
76fa82c3a7 | ||
|
|
46c9774efa | ||
|
|
1bc64a23e3 | ||
|
|
cc9a17afa3 | ||
|
|
fcbcfc3a03 | ||
|
|
0144f3b131 | ||
|
|
518436d5ca | ||
|
|
1961a6b913 | ||
|
|
5edebdd5f3 | ||
|
|
48edf2c212 | ||
|
|
9b16dca9da | ||
|
|
9479c2498c | ||
|
|
4a18bcabe2 | ||
|
|
34f690c53d | ||
|
|
e70cd6579f | ||
|
|
99c047e9c7 | ||
|
|
978ae326f8 | ||
|
|
b01f27f2d3 | ||
|
|
4d34e0dd96 | ||
|
|
9b0065e533 | ||
|
|
34a51feee9 | ||
|
|
679091de59 | ||
|
|
c6220cb920 | ||
|
|
633b30269c | ||
|
|
8ad26267d8 | ||
|
|
7cfb125682 | ||
|
|
a2dc5de5ce | ||
|
|
4f41ff29ec | ||
|
|
13d95b8c19 | ||
|
|
335c13ab44 | ||
|
|
015cdc10a7 | ||
|
|
a5075ab28f | ||
|
|
2e3c3274c7 | ||
|
|
443c581955 | ||
|
|
5e41faf590 | ||
|
|
0579fb83d3 | ||
|
|
2864845d5b | ||
|
|
659dddace7 | ||
|
|
b096f30558 | ||
|
|
dcfb72143a | ||
|
|
c63bcd7706 | ||
|
|
bac306f9b3 | ||
|
|
a6fd96e97b | ||
|
|
7edd91f923 | ||
|
|
66df445bd5 | ||
|
|
5c03034bb4 | ||
|
|
0eab181d46 | ||
|
|
f6f95308c7 | ||
|
|
e93cad91b4 | ||
|
|
08ee61b179 | ||
|
|
0ab756665e | ||
|
|
3570cddce0 | ||
|
|
a2d9f1a5db | ||
|
|
0c6a214e55 | ||
|
|
095fcdbe69 | ||
|
|
3afb1d18f6 | ||
|
|
4526c84330 | ||
|
|
a9cc1cb4b9 | ||
|
|
7acf1eace3 | ||
|
|
bb2e8cb287 | ||
|
|
3b389259c1 | ||
|
|
310ce55b09 | ||
|
|
4662e3fec2 | ||
|
|
10fab9d415 | ||
|
|
acecf01cbe | ||
|
|
b95fb9cda4 | ||
|
|
23fd595582 | ||
|
|
468d05cb10 | ||
|
|
88c2e5d160 | ||
|
|
8cf491b802 | ||
|
|
5886e78299 | ||
|
|
591ef9781c | ||
|
|
52fef96c1c | ||
|
|
d5fd9c57ec | ||
|
|
8a7d6a1c13 | ||
|
|
1745c72981 | ||
|
|
b98b7646cb | ||
|
|
1446e925ec | ||
|
|
799cda1e3e | ||
|
|
d2e0231341 | ||
|
|
9e0aa655a0 | ||
|
|
e303c73d44 | ||
|
|
e5e8e66d6b | ||
|
|
0bb3381122 | ||
|
|
2c5bf2bd6b | ||
|
|
83763b00ae | ||
|
|
47fc795c97 | ||
|
|
6a59bf7af3 | ||
|
|
6f185570d6 | ||
|
|
8b8336d9d0 | ||
|
|
a1e1d6ed3f | ||
|
|
df3e209534 | ||
|
|
851408860e | ||
|
|
d92ea23b0e | ||
|
|
d83641db92 | ||
|
|
48f262e1f3 | ||
|
|
bc49a7db50 | ||
|
|
49975b461c | ||
|
|
3ccb635956 | ||
|
|
074a90e0ac | ||
|
|
526f927436 | ||
|
|
fae7fad915 | ||
|
|
56132ceb8b | ||
|
|
3c3fce0a5a | ||
|
|
8139384961 | ||
|
|
734cc94048 | ||
|
|
91dbb7770d | ||
|
|
707709837d | ||
|
|
e868e65208 | ||
|
|
b19ad2a13b | ||
|
|
98ccccf6ab | ||
|
|
0a6166c557 | ||
|
|
5ee00e52a8 | ||
|
|
ff2b4e0b2f | ||
|
|
8fdd8db51b | ||
|
|
f113eb7ba8 | ||
|
|
40bc25ed5f | ||
|
|
cd3eab5d35 | ||
|
|
7038e15ad2 | ||
|
|
c72abd29ec | ||
|
|
5efc3d2228 | ||
|
|
8ac3f83c79 | ||
|
|
b31b054d0c | ||
|
|
fb4a87f5ce | ||
|
|
049c73880b | ||
|
|
7b2c54f6a6 | ||
|
|
23bda39812 | ||
|
|
90af5801ee |
@@ -10,3 +10,4 @@ SECRET='whyour'
|
|||||||
QINIU_AK=''
|
QINIU_AK=''
|
||||||
QINIU_SK=''
|
QINIU_SK=''
|
||||||
QINIU_SCOPE=''
|
QINIU_SCOPE=''
|
||||||
|
TEMP=''
|
||||||
|
|||||||
@@ -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: '8.3.1'
|
||||||
|
|
||||||
- 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,6 +74,16 @@ 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:
|
||||||
needs: build-static
|
needs: build-static
|
||||||
|
|
||||||
@@ -55,7 +95,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: '8.3.1'
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Setup timezone
|
- name: Setup timezone
|
||||||
uses: zcong1993/setup-timezone@master
|
uses: zcong1993/setup-timezone@master
|
||||||
@@ -84,11 +129,14 @@ jobs:
|
|||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
# generate Docker tags based on the following events/attributes
|
# generate Docker tags based on the following events/attributes
|
||||||
# nightly, master, pr-2, 1.2.3, 1.2, 1
|
# nightly, master, pr-2, 1.2.3, 1.2, 1
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
tags: |
|
tags: |
|
||||||
type=schedule,pattern=nightly
|
type=schedule,pattern=nightly
|
||||||
type=edge
|
type=edge
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
|
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
@@ -106,9 +154,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,24 +1,28 @@
|
|||||||
import { defineConfig } from '@umijs/max';
|
import { defineConfig } from '@umijs/max';
|
||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
|
|
||||||
|
const baseUrl = process.env.QlBaseUrl || '/';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
hash: true,
|
hash: true,
|
||||||
antd: {},
|
antd: {},
|
||||||
outputPath: 'static/dist',
|
outputPath: 'static/dist',
|
||||||
fastRefresh: true,
|
fastRefresh: true,
|
||||||
favicons: ['/images/favicon.svg'],
|
favicons: [`https://qn.whyour.cn/favicon.svg`],
|
||||||
mfsu: {
|
mfsu: {
|
||||||
strategy: 'eager',
|
strategy: 'eager',
|
||||||
},
|
},
|
||||||
|
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/public': {
|
[`${baseUrl}api/public`]: {
|
||||||
target: 'http://127.0.0.1:5400/',
|
target: 'http://127.0.0.1:5400/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
|
||||||
},
|
},
|
||||||
'/api': {
|
[`${baseUrl}api`]: {
|
||||||
target: 'http://127.0.0.1:5600/',
|
target: 'http://127.0.0.1:5600/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
|
pathRewrite: { [`^${baseUrl}api`]: '/api' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
chainWebpack: ((config: any) => {
|
chainWebpack: ((config: any) => {
|
||||||
@@ -36,6 +40,7 @@ export default defineConfig({
|
|||||||
'react-dom': 'window.ReactDOM',
|
'react-dom': 'window.ReactDOM',
|
||||||
},
|
},
|
||||||
headScripts: [
|
headScripts: [
|
||||||
|
`./api/env.js`,
|
||||||
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
|
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
|
||||||
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
|
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
|
||||||
],
|
],
|
||||||
|
|||||||
+62
-98
@@ -1,30 +1,29 @@
|
|||||||
<p align="center">
|
|
||||||
<a href="https://github.com/whyour/qinglong">
|
|
||||||
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">Green Dragon</h1>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||||
|
|
||||||
Timed task management panel with python3, javaScript, shell, typescript support
|
<h1 align="center">Qinglong</h1>
|
||||||
|
|
||||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
[简体中文](./README.md) | English
|
||||||
|
|
||||||
|
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||||
|
|
||||||
|
[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||||
|
|
||||||
|
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
|
||||||
|
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
|
||||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
|
||||||
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
|
|
||||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
||||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
|
|
||||||
|
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
||||||
|
|
||||||
|
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[](https://whyour.cn)
|

|
||||||
|
|
||||||
[简体中文](./README.md) | English
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -38,62 +37,10 @@ Timed task management panel with python3, javaScript, shell, typescript support
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Local Deployment
|
### Docker (Recommended)
|
||||||
|
|
||||||
```bash
|
|
||||||
# To be completed
|
|
||||||
```
|
|
||||||
|
|
||||||
### Podman Deployment
|
|
||||||
|
|
||||||
1. podman installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
https://podman.io/getting-started/installation
|
|
||||||
```
|
|
||||||
|
|
||||||
2. start the container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
podman run -dit \
|
|
||||||
--network bridge \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
-p 5700:5700 \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
docker.io/whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Deployment
|
|
||||||
|
|
||||||
1. docker installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -sSL get.docker.com | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. configure domestic mirror sources
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /etc/docker
|
|
||||||
tee /etc/docker/daemon.json <<-'EOF'
|
|
||||||
{
|
|
||||||
"registry-mirrors": [
|
|
||||||
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
|
||||||
"https://ypzju6vq.mirror.aliyuncs.com",
|
|
||||||
"https://registry.docker-cn.com",
|
|
||||||
"http://hub-mirror.c.163.com",
|
|
||||||
"https://docker.mirrors.ustc.edu.cn"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart docker
|
|
||||||
```
|
|
||||||
|
|
||||||
3. start the container
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# curl -sSL get.docker.com | sh
|
||||||
docker run -dit \
|
docker run -dit \
|
||||||
-v $PWD/ql/data:/ql/data \
|
-v $PWD/ql/data:/ql/data \
|
||||||
-p 5700:5700 \
|
-p 5700:5700 \
|
||||||
@@ -103,17 +50,10 @@ docker run -dit \
|
|||||||
whyour/qinglong:latest
|
whyour/qinglong:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker-compose Deployment
|
### Docker-compose (Recommended)
|
||||||
|
|
||||||
1. docker-compose installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
2. start the container
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
mkdir qinglong
|
mkdir qinglong
|
||||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||||
|
|
||||||
@@ -123,9 +63,32 @@ docker-compose up -d
|
|||||||
docker-compose down
|
docker-compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
3. access
|
### Podman (Recommended)
|
||||||
|
|
||||||
Open your browser and visit http://{ip}:5700
|
```bash
|
||||||
|
# https://podman.io/getting-started/installation
|
||||||
|
podman run -dit \
|
||||||
|
--network bridge \
|
||||||
|
-v $PWD/ql/data:/ql/data \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
docker.io/whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local
|
||||||
|
|
||||||
|
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3 yourself
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @whyour/qinglong
|
||||||
|
qinglong
|
||||||
|
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
|
||||||
|
export QL_DIR=""
|
||||||
|
export QL_DATA_DIR=""
|
||||||
|
# Run again
|
||||||
|
qinglong
|
||||||
|
```
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
@@ -161,6 +124,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
|
||||||
@@ -175,6 +142,21 @@ 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
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone git@github.com:whyour/qinglong.git
|
||||||
|
$ cd qinglong
|
||||||
|
$ cp .env.example .env
|
||||||
|
# Recommended use pnpm https://pnpm.io/zh/installation
|
||||||
|
$ npm install -g pnpm@8.3.1
|
||||||
|
$ pnpm install
|
||||||
|
$ pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Open your browser and visit http://127.0.0.1:5700
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
@@ -186,24 +168,6 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
- [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)
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ git clone git@github.com:whyour/qinglong.git
|
|
||||||
$ cd qinglong
|
|
||||||
$ cp .env.example .env
|
|
||||||
# Recommended use pnpm https://pnpm.io/zh/installation
|
|
||||||
$ npm install -g pnpm
|
|
||||||
$ pnpm install
|
|
||||||
$ pnpm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Open your browser and visit http://127.0.0.1:5700
|
|
||||||
|
|
||||||
## Communication
|
|
||||||
|
|
||||||
[telegram channel](https://t.me/jiao_long)
|
|
||||||
|
|
||||||
## Name Origin
|
## Name Origin
|
||||||
|
|
||||||
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
|
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
<p align="center">
|
<div align="center">
|
||||||
<a href="https://github.com/whyour/qinglong">
|
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
||||||
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">青龙</h1>
|
<h1 align="center">青龙</h1>
|
||||||
|
|
||||||
<div align="center">
|
简体中文 | [English](./README-en.md)
|
||||||
|
|
||||||
支持python3、javaScript、shell、typescript 的定时任务管理面板
|
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台
|
||||||
|
|
||||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||||
|
|
||||||
|
[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||||
|
|
||||||
|
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
|
||||||
|
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
|
||||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
|
||||||
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
|
|
||||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
||||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
|
|
||||||
|
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
||||||
|
|
||||||
|
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[](https://whyour.cn)
|

|
||||||
|
|
||||||
简体中文 | [English](./README-en.md)
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
@@ -38,83 +39,25 @@
|
|||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
### 本机部署
|
### docker (推荐)
|
||||||
|
|
||||||
```bash
|
|
||||||
# 待完善
|
|
||||||
```
|
|
||||||
|
|
||||||
### podman 部署
|
|
||||||
|
|
||||||
1. podman 安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
https://podman.io/getting-started/installation
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 启动容器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
podman run -dit \
|
|
||||||
--network bridge \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
-p 5700:5700 \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
docker.io/whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### docker 部署
|
|
||||||
|
|
||||||
1. docker 安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -sSL get.docker.com | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 配置国内镜像源
|
|
||||||
Configure domestic mirror sources
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /etc/docker
|
|
||||||
tee /etc/docker/daemon.json <<-'EOF'
|
|
||||||
{
|
|
||||||
"registry-mirrors": [
|
|
||||||
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
|
||||||
"https://ypzju6vq.mirror.aliyuncs.com",
|
|
||||||
"https://registry.docker-cn.com",
|
|
||||||
"http://hub-mirror.c.163.com",
|
|
||||||
"https://docker.mirrors.ustc.edu.cn"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart docker
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 启动容器
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# curl -sSL get.docker.com | sh
|
||||||
docker run -dit \
|
docker run -dit \
|
||||||
-v $PWD/ql/data:/ql/data \
|
-v $PWD/ql/data:/ql/data \
|
||||||
-p 5700:5700 \
|
-p 5700:5700 \
|
||||||
|
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||||
|
-e QlBaseUrl="/" \
|
||||||
--name qinglong \
|
--name qinglong \
|
||||||
--hostname qinglong \
|
--hostname qinglong \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
whyour/qinglong:latest
|
whyour/qinglong:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### docker-compose 部署
|
### docker-compose (推荐)
|
||||||
|
|
||||||
1. docker-compose 安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 启动容器
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
mkdir qinglong
|
mkdir qinglong
|
||||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||||
|
|
||||||
@@ -124,9 +67,34 @@ docker-compose up -d
|
|||||||
docker-compose down
|
docker-compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 访问
|
### podman (推荐)
|
||||||
|
|
||||||
打开你的浏览器,访问 http://{ip}:5700
|
```bash
|
||||||
|
# https://podman.io/getting-started/installation
|
||||||
|
podman run -dit \
|
||||||
|
--network bridge \
|
||||||
|
-v $PWD/ql/data:/ql/data \
|
||||||
|
-p 5700:5700 \
|
||||||
|
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||||
|
-e QlBaseUrl="/" \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
docker.io/whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 本机
|
||||||
|
|
||||||
|
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @whyour/qinglong
|
||||||
|
qinglong
|
||||||
|
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
|
||||||
|
export QL_DIR=""
|
||||||
|
export QL_DATA_DIR=""
|
||||||
|
# 再次执行
|
||||||
|
qinglong
|
||||||
|
```
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
@@ -160,6 +128,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. 参数说明
|
||||||
@@ -175,6 +147,21 @@ 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"代表天
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone git@github.com:whyour/qinglong.git
|
||||||
|
$ cd qinglong
|
||||||
|
$ cp .env.example .env
|
||||||
|
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
||||||
|
$ npm install -g pnpm@8.3.1
|
||||||
|
$ pnpm install
|
||||||
|
$ pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
打开你的浏览器,访问 http://127.0.0.1:5700
|
||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
@@ -186,24 +173,6 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
- [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)
|
||||||
|
|
||||||
## 开发
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ git clone git@github.com:whyour/qinglong.git
|
|
||||||
$ cd qinglong
|
|
||||||
$ cp .env.example .env
|
|
||||||
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
|
||||||
$ npm install -g pnpm
|
|
||||||
$ pnpm install
|
|
||||||
$ pnpm start
|
|
||||||
```
|
|
||||||
|
|
||||||
打开你的浏览器,访问 http://127.0.0.1:5700
|
|
||||||
|
|
||||||
## 交流
|
|
||||||
|
|
||||||
[telegram频道](https://t.me/jiao_long)
|
|
||||||
|
|
||||||
## 名称来源
|
## 名称来源
|
||||||
|
|
||||||
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
||||||
|
|||||||
+4
-3
@@ -5,6 +5,7 @@ import { Logger } from 'winston';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import { join } from 'path';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
@@ -41,11 +42,11 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
if (req.params.file.includes('sample')) {
|
if (req.params.file.includes('sample')) {
|
||||||
content = getFileContentByName(
|
content = getFileContentByName(
|
||||||
`${config.samplePath}${req.params.file}`,
|
join(config.samplePath, req.params.file),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content = getFileContentByName(
|
content = getFileContentByName(
|
||||||
`${config.configPath}${req.params.file}`,
|
join(config.configPath, req.params.file),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
@@ -70,7 +71,7 @@ export default (app: Router) => {
|
|||||||
if (config.blackFileList.includes(name)) {
|
if (config.blackFileList.includes(name)) {
|
||||||
res.send({ code: 403, message: '文件无法访问' });
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
}
|
}
|
||||||
const path = `${config.configPath}${name}`;
|
const path = join(config.configPath, name);
|
||||||
fs.writeFileSync(path, content);
|
fs.writeFileSync(path, content);
|
||||||
res.send({ code: 200, message: '保存成功' });
|
res.send({ code: 200, message: '保存成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default (app: Router) => {
|
|||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
sorts: Joi.array().optional().allow(null),
|
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) => {
|
||||||
@@ -51,13 +52,18 @@ export default (app: Router) => {
|
|||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
sorts: Joi.array().optional().allow(null),
|
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);
|
||||||
}
|
}
|
||||||
@@ -146,6 +152,21 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/detail',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.find(req.query as any);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
@@ -154,6 +175,7 @@ export default (app: Router) => {
|
|||||||
schedule: Joi.string().required(),
|
schedule: Joi.string().required(),
|
||||||
name: Joi.string().optional(),
|
name: Joi.string().optional(),
|
||||||
labels: Joi.array().optional(),
|
labels: Joi.array().optional(),
|
||||||
|
sub_id: Joi.number().optional().allow(null),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -310,6 +332,7 @@ export default (app: Router) => {
|
|||||||
command: Joi.string().required(),
|
command: Joi.string().required(),
|
||||||
schedule: Joi.string().required(),
|
schedule: Joi.string().required(),
|
||||||
name: Joi.string().optional().allow(null),
|
name: Joi.string().optional().allow(null),
|
||||||
|
sub_id: Joi.number().optional().allow(null),
|
||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
+34
-7
@@ -94,7 +94,7 @@ export default (app: Router) => {
|
|||||||
path += '/';
|
path += '/';
|
||||||
}
|
}
|
||||||
if (!path.startsWith('/')) {
|
if (!path.startsWith('/')) {
|
||||||
path = `${config.scriptPath}${path}`;
|
path = join(config.scriptPath, path);
|
||||||
}
|
}
|
||||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||||
return res.send({
|
return res.send({
|
||||||
@@ -124,7 +124,7 @@ export default (app: Router) => {
|
|||||||
if (fs.existsSync(originFilePath)) {
|
if (fs.existsSync(originFilePath)) {
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
originFilePath,
|
originFilePath,
|
||||||
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
|
join(config.bakPath, originFilename.replace(/\//g, '')),
|
||||||
);
|
);
|
||||||
if (filename !== originFilename) {
|
if (filename !== originFilename) {
|
||||||
fs.unlinkSync(originFilePath);
|
fs.unlinkSync(originFilePath);
|
||||||
@@ -170,7 +170,7 @@ 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()
|
type: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -207,7 +207,7 @@ export default (app: Router) => {
|
|||||||
let { filename } = req.body as {
|
let { filename } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
};
|
};
|
||||||
const filePath = `${config.scriptPath}${filename}`;
|
const filePath = join(config.scriptPath, filename);
|
||||||
// const stats = fs.statSync(filePath);
|
// const stats = fs.statSync(filePath);
|
||||||
// res.set({
|
// res.set({
|
||||||
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
||||||
@@ -255,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(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
+90
-6
@@ -7,6 +7,14 @@ 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 {
|
||||||
|
getUniqPath,
|
||||||
|
handleLogPath,
|
||||||
|
parseVersion,
|
||||||
|
promiseExec,
|
||||||
|
} from '../config/util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
@@ -18,17 +26,28 @@ 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 (
|
||||||
Object.keys(authInfo).length === 2 &&
|
Object.keys(authInfo).length === 2 &&
|
||||||
authInfo.username === 'admin' &&
|
authInfo.username === 'admin' &&
|
||||||
authInfo.password === 'admin' &&
|
authInfo.password === 'admin'
|
||||||
envCount === 0
|
|
||||||
) {
|
) {
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
}
|
}
|
||||||
@@ -37,6 +56,11 @@ export default (app: Router) => {
|
|||||||
data: {
|
data: {
|
||||||
isInitialized,
|
isInitialized,
|
||||||
version,
|
version,
|
||||||
|
lastCommitTime: dayjs(lastCommitTime).unix(),
|
||||||
|
lastCommitId,
|
||||||
|
branch,
|
||||||
|
changeLog,
|
||||||
|
changeLogLink,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -127,4 +151,64 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/command-run',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
command: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const uniqPath = await getUniqPath(req.body.command);
|
||||||
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
|
const logPath = `${uniqPath}/${logTime}.log`;
|
||||||
|
res.setHeader('Content-type', 'application/octet-stream');
|
||||||
|
await systemService.run(
|
||||||
|
{ ...req.body, logPath },
|
||||||
|
{
|
||||||
|
onStart: async (cp, startTime) => {
|
||||||
|
res.setHeader('QL-Task-Pid', `${cp.pid}`);
|
||||||
|
},
|
||||||
|
onEnd: async (cp, endTime, diff) => {
|
||||||
|
res.end();
|
||||||
|
},
|
||||||
|
onError: async (message: string) => {
|
||||||
|
res.write(`\n${message}`);
|
||||||
|
const absolutePath = await handleLogPath(logPath);
|
||||||
|
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||||
|
},
|
||||||
|
onLog: async (message: string) => {
|
||||||
|
res.write(`\n${message}`);
|
||||||
|
const absolutePath = await handleLogPath(logPath);
|
||||||
|
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/command-stop',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
command: Joi.string().optional(),
|
||||||
|
pid: Joi.number().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.stop(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
|
|||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
@@ -26,6 +27,10 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/login',
|
'/login',
|
||||||
|
rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 100,
|
||||||
|
}),
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
username: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ async function startServer() {
|
|||||||
const server = app
|
const server = app
|
||||||
.listen(config.port, () => {
|
.listen(config.port, () => {
|
||||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
Logger.debug(`✌️ 后端服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
export const LOG_END_SYMBOL = '\n ';
|
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} `;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ if (!process.env.QL_DIR) {
|
|||||||
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
|
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastVersionFile = `https://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') });
|
||||||
@@ -27,6 +27,7 @@ const bakPath = path.join(dataPath, 'bak/');
|
|||||||
const logPath = path.join(dataPath, 'log/');
|
const logPath = path.join(dataPath, 'log/');
|
||||||
const dbPath = path.join(dataPath, 'db/');
|
const dbPath = path.join(dataPath, 'db/');
|
||||||
const uploadPath = path.join(dataPath, 'upload/');
|
const uploadPath = path.join(dataPath, 'upload/');
|
||||||
|
const sshdPath = path.join(dataPath, 'ssh.d/');
|
||||||
|
|
||||||
const envFile = path.join(configPath, 'env.sh');
|
const envFile = path.join(configPath, 'env.sh');
|
||||||
const confFile = path.join(configPath, 'config.sh');
|
const confFile = path.join(configPath, 'config.sh');
|
||||||
@@ -40,7 +41,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 ⚠️");
|
||||||
@@ -91,8 +92,14 @@ export default {
|
|||||||
'/api/system',
|
'/api/system',
|
||||||
'/api/user/init',
|
'/api/user/init',
|
||||||
'/api/user/notification/init',
|
'/api/user/notification/init',
|
||||||
|
'/open/user/login',
|
||||||
|
'/open/user/two-factor/login',
|
||||||
|
'/open/system',
|
||||||
|
'/open/user/init',
|
||||||
|
'/open/user/notification/init',
|
||||||
],
|
],
|
||||||
versionFile,
|
versionFile,
|
||||||
lastVersionFile,
|
lastVersionFile,
|
||||||
sqliteFile,
|
sqliteFile,
|
||||||
|
sshdPath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { pick } from 'lodash';
|
||||||
|
|
||||||
|
let pickedEnv: Record<string, string>;
|
||||||
|
|
||||||
|
function getPickedEnv() {
|
||||||
|
if (pickedEnv) return pickedEnv;
|
||||||
|
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
|
||||||
|
pickedEnv = picked as Record<string, string>;
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serveEnv(_req: Request, res: Response) {
|
||||||
|
res.type('.js');
|
||||||
|
res.send(
|
||||||
|
Object.entries(getPickedEnv())
|
||||||
|
.map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)
|
||||||
|
.join('\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Subscription } from '../data/subscription';
|
||||||
|
import isNil from 'lodash/isNil';
|
||||||
|
|
||||||
|
export function formatUrl(doc: Subscription) {
|
||||||
|
let url = doc.url;
|
||||||
|
let host = '';
|
||||||
|
if (doc.type === 'private-repo') {
|
||||||
|
if (doc.pull_type === 'ssh-key') {
|
||||||
|
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
|
||||||
|
url = doc.url!.replace(host, doc.alias);
|
||||||
|
} else {
|
||||||
|
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
|
||||||
|
const { username, password } = doc.pull_option as any;
|
||||||
|
url = doc.url!.replace(host, `${username}:${password}@${host}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { url, host };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCommand(doc: Subscription, url?: string) {
|
||||||
|
let command = `SUB_ID=${doc.id} ql `;
|
||||||
|
let _url = url || formatUrl(doc).url;
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
whitelist,
|
||||||
|
blacklist,
|
||||||
|
dependences,
|
||||||
|
branch,
|
||||||
|
extensions,
|
||||||
|
proxy,
|
||||||
|
autoAddCron,
|
||||||
|
autoDelCron,
|
||||||
|
} = doc;
|
||||||
|
if (type === 'file') {
|
||||||
|
command += `raw "${_url}"`;
|
||||||
|
} else {
|
||||||
|
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${dependences || ''
|
||||||
|
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||||
|
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
+117
-7
@@ -4,6 +4,11 @@ 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 FormData from 'form-data';
|
||||||
|
import psTreeFun from 'pstree.remy';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { load } from 'js-yaml';
|
||||||
|
import config from './index';
|
||||||
|
import { TASK_COMMAND } from './const';
|
||||||
|
|
||||||
export function getFileContentByName(fileName: string) {
|
export function getFileContentByName(fileName: string) {
|
||||||
if (fs.existsSync(fileName)) {
|
if (fs.existsSync(fileName)) {
|
||||||
@@ -242,6 +247,18 @@ export async function createFile(file: string, data: string = '') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function handleLogPath(
|
||||||
|
logPath: string,
|
||||||
|
data: string = '',
|
||||||
|
): Promise<string> {
|
||||||
|
const absolutePath = path.resolve(config.logPath, logPath);
|
||||||
|
const logFileExist = await fileExist(absolutePath);
|
||||||
|
if (!logFileExist) {
|
||||||
|
await createFile(absolutePath, data);
|
||||||
|
}
|
||||||
|
return absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
export async function concurrentRun(
|
export async function concurrentRun(
|
||||||
fnList: Array<() => Promise<any>> = [],
|
fnList: Array<() => Promise<any>> = [],
|
||||||
max = 5,
|
max = 5,
|
||||||
@@ -284,6 +301,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 = '',
|
||||||
@@ -303,10 +334,8 @@ export function readDirs(
|
|||||||
key,
|
key,
|
||||||
type: 'directory',
|
type: 'directory',
|
||||||
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 {
|
||||||
@@ -315,11 +344,10 @@ export function readDirs(
|
|||||||
isLeaf: true,
|
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(
|
||||||
@@ -441,3 +469,85 @@ export function parseBody(
|
|||||||
|
|
||||||
return parsed;
|
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);
|
||||||
|
// SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
|
||||||
|
if (pids.length) {
|
||||||
|
try {
|
||||||
|
[pid, ...pids].forEach((x) => {
|
||||||
|
process.kill(x, 2);
|
||||||
|
});
|
||||||
|
} catch (error) {}
|
||||||
|
} else {
|
||||||
|
process.kill(pid, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPid(name: string) {
|
||||||
|
const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
|
||||||
|
const pid = await promiseExec(taskCommand);
|
||||||
|
return pid ? Number(pid) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IVersion {
|
||||||
|
version: string;
|
||||||
|
changeLogLink: string;
|
||||||
|
changeLog: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseVersion(path: string): Promise<IVersion> {
|
||||||
|
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseContentVersion(content: string): Promise<IVersion> {
|
||||||
|
return load(content) as IVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUniqPath(command: string): Promise<string> {
|
||||||
|
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
|
||||||
|
let id = await promiseExec(idStr);
|
||||||
|
|
||||||
|
if (/^\d\d*\d$/.test(id)) {
|
||||||
|
id = `_${id}`;
|
||||||
|
} else {
|
||||||
|
id = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = command.split(/ +/);
|
||||||
|
let str = items[0];
|
||||||
|
if (items[0] === TASK_COMMAND) {
|
||||||
|
str = items[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dotIndex = str.lastIndexOf('.');
|
||||||
|
|
||||||
|
if (dotIndex !== -1) {
|
||||||
|
str = str.slice(0, dotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slashIndex = str.lastIndexOf('/');
|
||||||
|
|
||||||
|
let tempStr = '';
|
||||||
|
if (slashIndex !== -1) {
|
||||||
|
tempStr = str.slice(0, slashIndex);
|
||||||
|
const _slashIndex = tempStr.lastIndexOf('/');
|
||||||
|
if (_slashIndex !== -1) {
|
||||||
|
tempStr = tempStr.slice(_slashIndex + 1);
|
||||||
|
}
|
||||||
|
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${str}${id}`;
|
||||||
|
}
|
||||||
|
|||||||
+4
-1
@@ -17,6 +17,7 @@ export class Crontab {
|
|||||||
labels?: string[];
|
labels?: string[];
|
||||||
last_running_time?: number;
|
last_running_time?: number;
|
||||||
last_execution_time?: number;
|
last_execution_time?: number;
|
||||||
|
sub_id?: number;
|
||||||
|
|
||||||
constructor(options: Crontab) {
|
constructor(options: Crontab) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
@@ -25,7 +26,7 @@ export class Crontab {
|
|||||||
this.saved = options.saved;
|
this.saved = options.saved;
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.status =
|
this.status =
|
||||||
options.status && CrontabStatus[options.status]
|
typeof options.status === 'number' && CrontabStatus[options.status]
|
||||||
? options.status
|
? options.status
|
||||||
: CrontabStatus.idle;
|
: CrontabStatus.idle;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
@@ -37,6 +38,7 @@ export class Crontab {
|
|||||||
this.labels = options.labels || [];
|
this.labels = options.labels || [];
|
||||||
this.last_running_time = options.last_running_time || 0;
|
this.last_running_time = options.last_running_time || 0;
|
||||||
this.last_execution_time = options.last_execution_time || 0;
|
this.last_execution_time = options.last_execution_time || 0;
|
||||||
|
this.sub_id = options.sub_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,4 +74,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
|||||||
labels: DataTypes.JSON,
|
labels: DataTypes.JSON,
|
||||||
last_running_time: DataTypes.NUMBER,
|
last_running_time: DataTypes.NUMBER,
|
||||||
last_execution_time: DataTypes.NUMBER,
|
last_execution_time: DataTypes.NUMBER,
|
||||||
|
sub_id: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
});
|
});
|
||||||
|
|||||||
+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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ export class Dependence {
|
|||||||
|
|
||||||
constructor(options: Dependence) {
|
constructor(options: Dependence) {
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.status = options.status || DependenceStatus.installing;
|
this.status =
|
||||||
|
typeof options.status === 'number' && DependenceStatus[options.status]
|
||||||
|
? options.status
|
||||||
|
: DependenceStatus.queued;
|
||||||
this.type = options.type || DependenceTypes.nodejs;
|
this.type = options.type || DependenceTypes.nodejs;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
@@ -28,6 +31,7 @@ export enum DependenceStatus {
|
|||||||
'removing',
|
'removing',
|
||||||
'removed',
|
'removed',
|
||||||
'removeFailed',
|
'removeFailed',
|
||||||
|
'queued',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DependenceTypes {
|
export enum DependenceTypes {
|
||||||
|
|||||||
+5
-2
@@ -13,7 +13,10 @@ export class Env {
|
|||||||
constructor(options: Env) {
|
constructor(options: Env) {
|
||||||
this.value = options.value;
|
this.value = options.value;
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.status = options.status || EnvStatus.normal;
|
this.status =
|
||||||
|
typeof options.status === 'number' && EnvStatus[options.status]
|
||||||
|
? options.status
|
||||||
|
: EnvStatus.normal;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.position = options.position;
|
this.position = options.position;
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
@@ -28,7 +31,7 @@ export enum EnvStatus {
|
|||||||
|
|
||||||
export const maxPosition = 9000000000000000;
|
export const maxPosition = 9000000000000000;
|
||||||
export const initPosition = 4500000000000000;
|
export const initPosition = 4500000000000000;
|
||||||
export const stepPosition = 10000000;
|
export const stepPosition = 10000000000;
|
||||||
export const minPosition = 100;
|
export const minPosition = 100;
|
||||||
|
|
||||||
interface EnvInstance extends Model<Env, Env>, Env {}
|
interface EnvInstance extends Model<Env, Env>, Env {}
|
||||||
|
|||||||
+2
-1
@@ -1,9 +1,10 @@
|
|||||||
import { Sequelize, Transaction } from 'sequelize';
|
import { Sequelize, Transaction } from 'sequelize';
|
||||||
import config from '../config/index';
|
import config from '../config/index';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
export const sequelize = new Sequelize({
|
export const sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: `${config.dbPath}database.sqlite`,
|
storage: join(config.dbPath, 'database.sqlite'),
|
||||||
logging: false,
|
logging: false,
|
||||||
retry: {
|
retry: {
|
||||||
max: 10,
|
max: 10,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ 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 {
|
export class ChatNotification extends NotificationBaseInfo {
|
||||||
@@ -71,10 +72,12 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
|
|||||||
|
|
||||||
export class WeWorkBotNotification extends NotificationBaseInfo {
|
export class WeWorkBotNotification extends NotificationBaseInfo {
|
||||||
public weWorkBotKey = '';
|
public weWorkBotKey = '';
|
||||||
|
public weWorkOrigin = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WeWorkAppNotification extends NotificationBaseInfo {
|
export class WeWorkAppNotification extends NotificationBaseInfo {
|
||||||
public weWorkAppKey = '';
|
public weWorkAppKey = '';
|
||||||
|
public weWorkOrigin = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AibotkNotification extends NotificationBaseInfo {
|
export class AibotkNotification extends NotificationBaseInfo {
|
||||||
|
|||||||
@@ -26,13 +26,6 @@ export interface AppToken {
|
|||||||
|
|
||||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
||||||
|
|
||||||
export enum CrontabStatus {
|
|
||||||
'running',
|
|
||||||
'idle',
|
|
||||||
'disabled',
|
|
||||||
'queued',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AppInstance extends Model<App, App>, App {}
|
interface AppInstance extends Model<App, App>, App {}
|
||||||
export const AppModel = sequelize.define<AppInstance>('App', {
|
export const AppModel = sequelize.define<AppInstance>('App', {
|
||||||
name: { type: DataTypes.STRING, unique: 'name' },
|
name: { type: DataTypes.STRING, unique: 'name' },
|
||||||
|
|||||||
@@ -28,14 +28,17 @@ 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;
|
||||||
this.name = options.name || options.alias;
|
this.name = options.name || options.alias;
|
||||||
this.type = options.type;
|
this.type = options.type;
|
||||||
this.schedule = options.schedule;
|
this.schedule = options.schedule;
|
||||||
this.status =
|
this.status = this.status =
|
||||||
options.status && SubscriptionStatus[options.status]
|
typeof options.status === 'number' && SubscriptionStatus[options.status]
|
||||||
? options.status
|
? options.status
|
||||||
: SubscriptionStatus.idle;
|
: SubscriptionStatus.idle;
|
||||||
this.url = options.url;
|
this.url = options.url;
|
||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+28
-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,33 @@ 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) {}
|
||||||
|
try {
|
||||||
|
await sequelize.query('alter table Crontabs add column sub_id 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');
|
||||||
|
|||||||
+13
-25
@@ -10,26 +10,27 @@ import Container from 'typedi';
|
|||||||
import OpenService from '../services/open';
|
import OpenService from '../services/open';
|
||||||
import rewrite from 'express-urlrewrite';
|
import rewrite from 'express-urlrewrite';
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
import handler from 'serve-handler';
|
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { EnvModel } from '../data/env';
|
import { EnvModel } from '../data/env';
|
||||||
import { errors } from 'celebrate';
|
import { errors } from 'celebrate';
|
||||||
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||||
|
import { serveEnv } from '../config/serverEnv';
|
||||||
|
|
||||||
export default ({ app }: { app: Application }) => {
|
export default ({ app }: { app: Application }) => {
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
app.get(`${config.api.prefix}/env.js`, serveEnv);
|
||||||
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use(
|
||||||
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
'/api/public',
|
||||||
next();
|
createProxyMiddleware({
|
||||||
} else {
|
target: `http://localhost:${config.publicPort}/api`,
|
||||||
return handler(req, res, {
|
changeOrigin: true,
|
||||||
public: 'static/dist',
|
pathRewrite: { '/api/public': '' },
|
||||||
rewrites: [{ source: '**', destination: '/index.html' }],
|
}),
|
||||||
});
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
|
|
||||||
@@ -108,8 +109,7 @@ export default ({ app }: { app: Application }) => {
|
|||||||
if (
|
if (
|
||||||
Object.keys(authInfo).length === 2 &&
|
Object.keys(authInfo).length === 2 &&
|
||||||
authInfo.username === 'admin' &&
|
authInfo.username === 'admin' &&
|
||||||
authInfo.password === 'admin' &&
|
authInfo.password === 'admin'
|
||||||
envCount === 0
|
|
||||||
) {
|
) {
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
}
|
}
|
||||||
@@ -170,18 +170,6 @@ export default ({ app }: { app: Application }) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(
|
|
||||||
Sentry.Handlers.errorHandler({
|
|
||||||
shouldHandleError(error) {
|
|
||||||
// 排除 SequelizeUniqueConstraintError / NotFound
|
|
||||||
return (
|
|
||||||
!['SequelizeUniqueConstraintError'].includes(error.name) ||
|
|
||||||
!['Not Found'].includes(error.message)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
err: Error & { status: number },
|
err: Error & { status: number },
|
||||||
|
|||||||
@@ -4,16 +4,31 @@ 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 groupBy from 'lodash/groupBy';
|
import { DependenceModel, DependenceStatus } 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,13 +41,11 @@ export default async () => {
|
|||||||
order: [['type', 'DESC']],
|
order: [['type', 'DESC']],
|
||||||
raw: true,
|
raw: true,
|
||||||
}).then(async (docs) => {
|
}).then(async (docs) => {
|
||||||
const groups = groupBy(docs, 'type');
|
await DependenceModel.update(
|
||||||
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
|
{ status: DependenceStatus.queued, log: [] },
|
||||||
for (const key of keys) {
|
{ where: { id: docs.map((x) => x.id!) } },
|
||||||
const group = groups[key];
|
);
|
||||||
const depIds = group.map((x) => x.id);
|
dependenceService.installDependenceOneByOne(docs);
|
||||||
await dependenceService.reInstall(depIds as number[]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化时执行一次所有的 ql repo 任务
|
// 初始化时执行一次所有的 ql repo 任务
|
||||||
|
|||||||
@@ -13,12 +13,18 @@ const logPath = path.join(dataPath, 'log/');
|
|||||||
const uploadPath = path.join(dataPath, 'upload/');
|
const uploadPath = path.join(dataPath, 'upload/');
|
||||||
const bakPath = path.join(dataPath, 'bak/');
|
const bakPath = path.join(dataPath, 'bak/');
|
||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
|
const tmpPath = path.join(logPath, '.tmp/');
|
||||||
const confFile = path.join(configPath, 'config.sh');
|
const confFile = path.join(configPath, 'config.sh');
|
||||||
const authConfigFile = path.join(configPath, 'auth.json');
|
const authConfigFile = path.join(configPath, 'auth.json');
|
||||||
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
||||||
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
|
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
|
||||||
|
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
|
||||||
|
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
|
||||||
|
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
|
||||||
|
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
|
||||||
const homedir = os.homedir();
|
const homedir = os.homedir();
|
||||||
const sshPath = path.resolve(homedir, '.ssh');
|
const sshPath = path.resolve(homedir, '.ssh');
|
||||||
|
const sshdPath = path.join(dataPath, 'ssh.d');
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const authFileExist = await fileExist(authConfigFile);
|
const authFileExist = await fileExist(authConfigFile);
|
||||||
@@ -29,19 +35,15 @@ export default async () => {
|
|||||||
const uploadDirExist = await fileExist(uploadPath);
|
const uploadDirExist = await fileExist(uploadPath);
|
||||||
const sshDirExist = await fileExist(sshPath);
|
const sshDirExist = await fileExist(sshPath);
|
||||||
const bakDirExist = await fileExist(bakPath);
|
const bakDirExist = await fileExist(bakPath);
|
||||||
|
const sshdDirExist = await fileExist(sshdPath);
|
||||||
|
const tmpDirExist = await fileExist(tmpPath);
|
||||||
|
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
|
||||||
|
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
|
||||||
|
|
||||||
if (!configDirExist) {
|
if (!configDirExist) {
|
||||||
fs.mkdirSync(configPath);
|
fs.mkdirSync(configPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authFileExist) {
|
|
||||||
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confFileExist) {
|
|
||||||
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!scriptDirExist) {
|
if (!scriptDirExist) {
|
||||||
fs.mkdirSync(scriptPath);
|
fs.mkdirSync(scriptPath);
|
||||||
}
|
}
|
||||||
@@ -50,6 +52,10 @@ export default async () => {
|
|||||||
fs.mkdirSync(logPath);
|
fs.mkdirSync(logPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!tmpDirExist) {
|
||||||
|
fs.mkdirSync(tmpPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (!uploadDirExist) {
|
if (!uploadDirExist) {
|
||||||
fs.mkdirSync(uploadPath);
|
fs.mkdirSync(uploadPath);
|
||||||
}
|
}
|
||||||
@@ -62,6 +68,27 @@ export default async () => {
|
|||||||
fs.mkdirSync(bakPath);
|
fs.mkdirSync(bakPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sshdDirExist) {
|
||||||
|
fs.mkdirSync(sshdPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化文件
|
||||||
|
if (!authFileExist) {
|
||||||
|
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confFileExist) {
|
||||||
|
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scriptNotifyJsFileExist) {
|
||||||
|
fs.writeFileSync(scriptNotifyJsFile, fs.readFileSync(sampleNotifyJsFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scriptNotifyPyFileExist) {
|
||||||
|
fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile));
|
||||||
|
}
|
||||||
|
|
||||||
dotenv.config({ path: confFile });
|
dotenv.config({ path: confFile });
|
||||||
|
|
||||||
Logger.info('✌️ Init file down');
|
Logger.info('✌️ Init file down');
|
||||||
|
|||||||
+10
-10
@@ -4,6 +4,7 @@ import ScheduleService from '../services/schedule';
|
|||||||
import SubscriptionService from '../services/subscription';
|
import SubscriptionService from '../services/subscription';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { fileExist } from '../config/util';
|
import { fileExist } from '../config/util';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
@@ -11,8 +12,9 @@ export default async () => {
|
|||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
|
||||||
// 生成内置token
|
// 生成内置token
|
||||||
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
|
let tokenCommand = `tsx ${join(config.rootPath, 'back/token.ts')}`;
|
||||||
const tokenFile = `${config.rootPath}static/build/token.js`;
|
const tokenFile = join(config.rootPath, 'static/build/token.js');
|
||||||
|
|
||||||
if (await fileExist(tokenFile)) {
|
if (await fileExist(tokenFile)) {
|
||||||
tokenCommand = `node ${tokenFile}`;
|
tokenCommand = `node ${tokenFile}`;
|
||||||
}
|
}
|
||||||
@@ -21,6 +23,7 @@ export default async () => {
|
|||||||
name: '生成token',
|
name: '生成token',
|
||||||
command: tokenCommand,
|
command: tokenCommand,
|
||||||
};
|
};
|
||||||
|
await scheduleService.cancelIntervalTask(cron);
|
||||||
scheduleService.createIntervalTask(cron, {
|
scheduleService.createIntervalTask(cron, {
|
||||||
days: 28,
|
days: 28,
|
||||||
});
|
});
|
||||||
@@ -28,24 +31,21 @@ export default async () => {
|
|||||||
// 运行删除日志任务
|
// 运行删除日志任务
|
||||||
const data = await systemService.getLogRemoveFrequency();
|
const data = await systemService.getLogRemoveFrequency();
|
||||||
if (data && data.info && data.info.frequency) {
|
if (data && data.info && data.info.frequency) {
|
||||||
const cron = {
|
const rmlogCron = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: '删除日志',
|
name: '删除日志',
|
||||||
command: `ql rmlog ${data.info.frequency}`,
|
command: `ql rmlog ${data.info.frequency}`,
|
||||||
};
|
};
|
||||||
scheduleService.createIntervalTask(cron, {
|
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||||
|
scheduleService.createIntervalTask(rmlogCron, {
|
||||||
days: data.info.frequency,
|
days: data.info.frequency,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行所有订阅
|
// 运行所有订阅
|
||||||
|
await subscriptionService.setSshConfig();
|
||||||
const subs = await subscriptionService.list();
|
const subs = await subscriptionService.list();
|
||||||
for (const sub of subs) {
|
for (const sub of subs) {
|
||||||
await subscriptionService.handleTask(
|
subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
|
||||||
sub,
|
|
||||||
!sub.is_disabled,
|
|
||||||
true,
|
|
||||||
!sub.is_disabled,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-7
@@ -4,21 +4,25 @@ import * as Tracing from '@sentry/tracing';
|
|||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { parseVersion } from '../config/util';
|
||||||
|
|
||||||
export default ({ expressApp }: { expressApp: Application }) => {
|
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
const { version } = await parseVersion(config.versionFile);
|
||||||
|
|
||||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
|
||||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
ignoreErrors: [
|
||||||
|
/SequelizeUniqueConstraintError/i,
|
||||||
|
/Validation error/i,
|
||||||
|
/UnauthorizedError/i,
|
||||||
|
/celebrate request validation failed/i,
|
||||||
|
],
|
||||||
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: 0.8,
|
||||||
release: currentVersion,
|
release: version,
|
||||||
});
|
});
|
||||||
|
|
||||||
expressApp.use(Sentry.Handlers.requestHandler());
|
expressApp.use(Sentry.Handlers.requestHandler());
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ export default async ({ server }: { server: Server }) => {
|
|||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
Logger.info('✌️ Server need close');
|
Logger.info('✌️ Server need close');
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
Logger.info('✌️ Server closed');
|
setTimeout(() => {
|
||||||
process.exit(0);
|
process.exit();
|
||||||
|
}, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Forcing server close !!!');
|
||||||
|
process.exit(1);
|
||||||
|
}, 15000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package com.ql.cron;
|
||||||
|
|
||||||
|
service Cron {
|
||||||
|
rpc addCron(AddCronRequest) returns (AddCronResponse);
|
||||||
|
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message ICron {
|
||||||
|
string id = 1;
|
||||||
|
string schedule = 2;
|
||||||
|
string command = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddCronRequest { repeated ICron crons = 1; }
|
||||||
|
|
||||||
|
message AddCronResponse {}
|
||||||
|
|
||||||
|
message DeleteCronRequest { repeated string ids = 1; }
|
||||||
|
|
||||||
|
message DeleteCronResponse {}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import {
|
||||||
|
CallOptions,
|
||||||
|
ChannelCredentials,
|
||||||
|
Client,
|
||||||
|
ClientOptions,
|
||||||
|
ClientUnaryCall,
|
||||||
|
handleUnaryCall,
|
||||||
|
makeGenericClientConstructor,
|
||||||
|
Metadata,
|
||||||
|
ServiceError,
|
||||||
|
UntypedServiceImplementation,
|
||||||
|
} from '@grpc/grpc-js';
|
||||||
|
import _m0 from 'protobufjs/minimal';
|
||||||
|
|
||||||
|
export const protobufPackage = 'com.ql.cron';
|
||||||
|
|
||||||
|
export interface ICron {
|
||||||
|
id: string;
|
||||||
|
schedule: string;
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddCronRequest {
|
||||||
|
crons: ICron[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddCronResponse {}
|
||||||
|
|
||||||
|
export interface DeleteCronRequest {
|
||||||
|
ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCronResponse {}
|
||||||
|
|
||||||
|
function createBaseICron(): ICron {
|
||||||
|
return { id: '', schedule: '', command: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ICron = {
|
||||||
|
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
if (message.id !== '') {
|
||||||
|
writer.uint32(10).string(message.id);
|
||||||
|
}
|
||||||
|
if (message.schedule !== '') {
|
||||||
|
writer.uint32(18).string(message.schedule);
|
||||||
|
}
|
||||||
|
if (message.command !== '') {
|
||||||
|
writer.uint32(26).string(message.command);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseICron();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag != 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.id = reader.string();
|
||||||
|
continue;
|
||||||
|
case 2:
|
||||||
|
if (tag != 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.schedule = reader.string();
|
||||||
|
continue;
|
||||||
|
case 3:
|
||||||
|
if (tag != 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.command = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): ICron {
|
||||||
|
return {
|
||||||
|
id: isSet(object.id) ? String(object.id) : '',
|
||||||
|
schedule: isSet(object.schedule) ? String(object.schedule) : '',
|
||||||
|
command: isSet(object.command) ? String(object.command) : '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: ICron): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
message.id !== undefined && (obj.id = message.id);
|
||||||
|
message.schedule !== undefined && (obj.schedule = message.schedule);
|
||||||
|
message.command !== undefined && (obj.command = message.command);
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
|
||||||
|
return ICron.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
|
||||||
|
const message = createBaseICron();
|
||||||
|
message.id = object.id ?? '';
|
||||||
|
message.schedule = object.schedule ?? '';
|
||||||
|
message.command = object.command ?? '';
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseAddCronRequest(): AddCronRequest {
|
||||||
|
return { crons: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddCronRequest = {
|
||||||
|
encode(
|
||||||
|
message: AddCronRequest,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
for (const v of message.crons) {
|
||||||
|
ICron.encode(v!, writer.uint32(10).fork()).ldelim();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseAddCronRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag != 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.crons.push(ICron.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): AddCronRequest {
|
||||||
|
return {
|
||||||
|
crons: Array.isArray(object?.crons)
|
||||||
|
? object.crons.map((e: any) => ICron.fromJSON(e))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: AddCronRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.crons) {
|
||||||
|
obj.crons = message.crons.map((e) => (e ? ICron.toJSON(e) : undefined));
|
||||||
|
} else {
|
||||||
|
obj.crons = [];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): AddCronRequest {
|
||||||
|
return AddCronRequest.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(
|
||||||
|
object: I,
|
||||||
|
): AddCronRequest {
|
||||||
|
const message = createBaseAddCronRequest();
|
||||||
|
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseAddCronResponse(): AddCronResponse {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddCronResponse = {
|
||||||
|
encode(
|
||||||
|
_: AddCronResponse,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseAddCronResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(_: any): AddCronResponse {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(_: AddCronResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): AddCronResponse {
|
||||||
|
return AddCronResponse.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(
|
||||||
|
_: I,
|
||||||
|
): AddCronResponse {
|
||||||
|
const message = createBaseAddCronResponse();
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseDeleteCronRequest(): DeleteCronRequest {
|
||||||
|
return { ids: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteCronRequest = {
|
||||||
|
encode(
|
||||||
|
message: DeleteCronRequest,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
for (const v of message.ids) {
|
||||||
|
writer.uint32(10).string(v!);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseDeleteCronRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag != 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.ids.push(reader.string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): DeleteCronRequest {
|
||||||
|
return {
|
||||||
|
ids: Array.isArray(object?.ids)
|
||||||
|
? object.ids.map((e: any) => String(e))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: DeleteCronRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.ids) {
|
||||||
|
obj.ids = message.ids.map((e) => e);
|
||||||
|
} else {
|
||||||
|
obj.ids = [];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): DeleteCronRequest {
|
||||||
|
return DeleteCronRequest.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
|
||||||
|
object: I,
|
||||||
|
): DeleteCronRequest {
|
||||||
|
const message = createBaseDeleteCronRequest();
|
||||||
|
message.ids = object.ids?.map((e) => e) || [];
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseDeleteCronResponse(): DeleteCronResponse {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteCronResponse = {
|
||||||
|
encode(
|
||||||
|
_: DeleteCronResponse,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseDeleteCronResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(_: any): DeleteCronResponse {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(_: DeleteCronResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): DeleteCronResponse {
|
||||||
|
return DeleteCronResponse.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
|
||||||
|
_: I,
|
||||||
|
): DeleteCronResponse {
|
||||||
|
const message = createBaseDeleteCronResponse();
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CronService = typeof CronService;
|
||||||
|
export const CronService = {
|
||||||
|
addCron: {
|
||||||
|
path: '/com.ql.cron.Cron/addCron',
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: false,
|
||||||
|
requestSerialize: (value: AddCronRequest) =>
|
||||||
|
Buffer.from(AddCronRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
|
||||||
|
responseSerialize: (value: AddCronResponse) =>
|
||||||
|
Buffer.from(AddCronResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
|
||||||
|
},
|
||||||
|
delCron: {
|
||||||
|
path: '/com.ql.cron.Cron/delCron',
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: false,
|
||||||
|
requestSerialize: (value: DeleteCronRequest) =>
|
||||||
|
Buffer.from(DeleteCronRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
|
||||||
|
responseSerialize: (value: DeleteCronResponse) =>
|
||||||
|
Buffer.from(DeleteCronResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface CronServer extends UntypedServiceImplementation {
|
||||||
|
addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;
|
||||||
|
delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CronClient extends Client {
|
||||||
|
addCron(
|
||||||
|
request: AddCronRequest,
|
||||||
|
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
addCron(
|
||||||
|
request: AddCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
addCron(
|
||||||
|
request: AddCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
delCron(
|
||||||
|
request: DeleteCronRequest,
|
||||||
|
callback: (
|
||||||
|
error: ServiceError | null,
|
||||||
|
response: DeleteCronResponse,
|
||||||
|
) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
delCron(
|
||||||
|
request: DeleteCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (
|
||||||
|
error: ServiceError | null,
|
||||||
|
response: DeleteCronResponse,
|
||||||
|
) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
delCron(
|
||||||
|
request: DeleteCronRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (
|
||||||
|
error: ServiceError | null,
|
||||||
|
response: DeleteCronResponse,
|
||||||
|
) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CronClient = makeGenericClientConstructor(
|
||||||
|
CronService,
|
||||||
|
'com.ql.cron.Cron',
|
||||||
|
) as unknown as {
|
||||||
|
new (
|
||||||
|
address: string,
|
||||||
|
credentials: ChannelCredentials,
|
||||||
|
options?: Partial<ClientOptions>,
|
||||||
|
): CronClient;
|
||||||
|
service: typeof CronService;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin =
|
||||||
|
| Date
|
||||||
|
| Function
|
||||||
|
| Uint8Array
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
export type DeepPartial<T> = T extends Builtin
|
||||||
|
? T
|
||||||
|
: T extends Array<infer U>
|
||||||
|
? Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U>
|
||||||
|
? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {}
|
||||||
|
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
export type Exact<P, I extends P> = P extends Builtin
|
||||||
|
? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
|
||||||
|
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package com.ql.health;
|
||||||
|
|
||||||
|
message HealthCheckRequest {
|
||||||
|
string service = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HealthCheckResponse {
|
||||||
|
enum ServingStatus {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
SERVING = 1;
|
||||||
|
NOT_SERVING = 2;
|
||||||
|
SERVICE_UNKNOWN = 3;
|
||||||
|
}
|
||||||
|
ServingStatus status = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Health {
|
||||||
|
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||||
|
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import {
|
||||||
|
CallOptions,
|
||||||
|
ChannelCredentials,
|
||||||
|
Client,
|
||||||
|
ClientOptions,
|
||||||
|
ClientReadableStream,
|
||||||
|
ClientUnaryCall,
|
||||||
|
handleServerStreamingCall,
|
||||||
|
handleUnaryCall,
|
||||||
|
makeGenericClientConstructor,
|
||||||
|
Metadata,
|
||||||
|
ServiceError,
|
||||||
|
UntypedServiceImplementation,
|
||||||
|
} from '@grpc/grpc-js';
|
||||||
|
import _m0 from 'protobufjs/minimal';
|
||||||
|
|
||||||
|
export const protobufPackage = 'com.ql.health';
|
||||||
|
|
||||||
|
export interface HealthCheckRequest {
|
||||||
|
service: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthCheckResponse {
|
||||||
|
status: HealthCheckResponse_ServingStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HealthCheckResponse_ServingStatus {
|
||||||
|
UNKNOWN = 0,
|
||||||
|
SERVING = 1,
|
||||||
|
NOT_SERVING = 2,
|
||||||
|
SERVICE_UNKNOWN = 3,
|
||||||
|
UNRECOGNIZED = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function healthCheckResponse_ServingStatusFromJSON(
|
||||||
|
object: any,
|
||||||
|
): HealthCheckResponse_ServingStatus {
|
||||||
|
switch (object) {
|
||||||
|
case 0:
|
||||||
|
case 'UNKNOWN':
|
||||||
|
return HealthCheckResponse_ServingStatus.UNKNOWN;
|
||||||
|
case 1:
|
||||||
|
case 'SERVING':
|
||||||
|
return HealthCheckResponse_ServingStatus.SERVING;
|
||||||
|
case 2:
|
||||||
|
case 'NOT_SERVING':
|
||||||
|
return HealthCheckResponse_ServingStatus.NOT_SERVING;
|
||||||
|
case 3:
|
||||||
|
case 'SERVICE_UNKNOWN':
|
||||||
|
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
|
||||||
|
case -1:
|
||||||
|
case 'UNRECOGNIZED':
|
||||||
|
default:
|
||||||
|
return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function healthCheckResponse_ServingStatusToJSON(
|
||||||
|
object: HealthCheckResponse_ServingStatus,
|
||||||
|
): string {
|
||||||
|
switch (object) {
|
||||||
|
case HealthCheckResponse_ServingStatus.UNKNOWN:
|
||||||
|
return 'UNKNOWN';
|
||||||
|
case HealthCheckResponse_ServingStatus.SERVING:
|
||||||
|
return 'SERVING';
|
||||||
|
case HealthCheckResponse_ServingStatus.NOT_SERVING:
|
||||||
|
return 'NOT_SERVING';
|
||||||
|
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
|
||||||
|
return 'SERVICE_UNKNOWN';
|
||||||
|
case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
|
||||||
|
default:
|
||||||
|
return 'UNRECOGNIZED';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseHealthCheckRequest(): HealthCheckRequest {
|
||||||
|
return { service: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthCheckRequest = {
|
||||||
|
encode(
|
||||||
|
message: HealthCheckRequest,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
if (message.service !== '') {
|
||||||
|
writer.uint32(10).string(message.service);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseHealthCheckRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag != 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.service = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): HealthCheckRequest {
|
||||||
|
return { service: isSet(object.service) ? String(object.service) : '' };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: HealthCheckRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
message.service !== undefined && (obj.service = message.service);
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): HealthCheckRequest {
|
||||||
|
return HealthCheckRequest.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
|
||||||
|
object: I,
|
||||||
|
): HealthCheckRequest {
|
||||||
|
const message = createBaseHealthCheckRequest();
|
||||||
|
message.service = object.service ?? '';
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseHealthCheckResponse(): HealthCheckResponse {
|
||||||
|
return { status: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthCheckResponse = {
|
||||||
|
encode(
|
||||||
|
message: HealthCheckResponse,
|
||||||
|
writer: _m0.Writer = _m0.Writer.create(),
|
||||||
|
): _m0.Writer {
|
||||||
|
if (message.status !== 0) {
|
||||||
|
writer.uint32(8).int32(message.status);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
|
||||||
|
const reader =
|
||||||
|
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseHealthCheckResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag != 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.status = reader.int32() as any;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) == 4 || tag == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): HealthCheckResponse {
|
||||||
|
return {
|
||||||
|
status: isSet(object.status)
|
||||||
|
? healthCheckResponse_ServingStatusFromJSON(object.status)
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: HealthCheckResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
message.status !== undefined &&
|
||||||
|
(obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
|
||||||
|
base?: I,
|
||||||
|
): HealthCheckResponse {
|
||||||
|
return HealthCheckResponse.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
|
||||||
|
object: I,
|
||||||
|
): HealthCheckResponse {
|
||||||
|
const message = createBaseHealthCheckResponse();
|
||||||
|
message.status = object.status ?? 0;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HealthService = typeof HealthService;
|
||||||
|
export const HealthService = {
|
||||||
|
check: {
|
||||||
|
path: '/com.ql.health.Health/Check',
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: false,
|
||||||
|
requestSerialize: (value: HealthCheckRequest) =>
|
||||||
|
Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
||||||
|
responseSerialize: (value: HealthCheckResponse) =>
|
||||||
|
Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
path: '/com.ql.health.Health/Watch',
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: true,
|
||||||
|
requestSerialize: (value: HealthCheckRequest) =>
|
||||||
|
Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
||||||
|
responseSerialize: (value: HealthCheckResponse) =>
|
||||||
|
Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface HealthServer extends UntypedServiceImplementation {
|
||||||
|
check: handleUnaryCall<HealthCheckRequest, HealthCheckResponse>;
|
||||||
|
watch: handleServerStreamingCall<HealthCheckRequest, HealthCheckResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthClient extends Client {
|
||||||
|
check(
|
||||||
|
request: HealthCheckRequest,
|
||||||
|
callback: (
|
||||||
|
error: ServiceError | null,
|
||||||
|
response: HealthCheckResponse,
|
||||||
|
) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
check(
|
||||||
|
request: HealthCheckRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (
|
||||||
|
error: ServiceError | null,
|
||||||
|
response: HealthCheckResponse,
|
||||||
|
) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
check(
|
||||||
|
request: HealthCheckRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (
|
||||||
|
error: ServiceError | null,
|
||||||
|
response: HealthCheckResponse,
|
||||||
|
) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
watch(
|
||||||
|
request: HealthCheckRequest,
|
||||||
|
options?: Partial<CallOptions>,
|
||||||
|
): ClientReadableStream<HealthCheckResponse>;
|
||||||
|
watch(
|
||||||
|
request: HealthCheckRequest,
|
||||||
|
metadata?: Metadata,
|
||||||
|
options?: Partial<CallOptions>,
|
||||||
|
): ClientReadableStream<HealthCheckResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthClient = makeGenericClientConstructor(
|
||||||
|
HealthService,
|
||||||
|
'com.ql.health.Health',
|
||||||
|
) as unknown as {
|
||||||
|
new (
|
||||||
|
address: string,
|
||||||
|
credentials: ChannelCredentials,
|
||||||
|
options?: Partial<ClientOptions>,
|
||||||
|
): HealthClient;
|
||||||
|
service: typeof HealthService;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin =
|
||||||
|
| Date
|
||||||
|
| Function
|
||||||
|
| Uint8Array
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
export type DeepPartial<T> = T extends Builtin
|
||||||
|
? T
|
||||||
|
: T extends Array<infer U>
|
||||||
|
? Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U>
|
||||||
|
? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {}
|
||||||
|
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
export type Exact<P, I extends P> = P extends Builtin
|
||||||
|
? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
|
||||||
|
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
+14
-11
@@ -1,20 +1,22 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { exec } from 'child_process';
|
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
import { HealthClient } from './protos/health';
|
||||||
|
import { credentials } from '@grpc/grpc-js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const client = new HealthClient(
|
||||||
app.get('/api/public/panel/log', (req, res) => {
|
`localhost:${config.cronPort}`,
|
||||||
exec(
|
credentials.createInsecure(),
|
||||||
'pm2 logs panel --lines 500 --nostream --timestamp',
|
|
||||||
(err, stdout, stderr) => {
|
|
||||||
if (err || stderr) {
|
|
||||||
return res.send({ code: 400, message: (err && err.message) || stderr });
|
|
||||||
}
|
|
||||||
return res.send({ code: 200, data: stdout });
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
client.check({ service: 'cron' }, (err, response) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).send({ code: 500, error: err });
|
||||||
|
}
|
||||||
|
return res.status(200).send({ code: 200, data: response });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app
|
app
|
||||||
@@ -23,6 +25,7 @@ app
|
|||||||
await require('./loaders/db').default();
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
Logger.debug(`✌️ 公共服务启动成功!`);
|
Logger.debug(`✌️ 公共服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import schedule from 'node-schedule';
|
|
||||||
import express from 'express';
|
|
||||||
import { exec } from 'child_process';
|
|
||||||
import Logger from './loaders/logger';
|
|
||||||
import { CrontabModel, CrontabStatus } from './data/cron';
|
|
||||||
import config from './config';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
const run = async () => {
|
|
||||||
CrontabModel.findAll({ where: {} })
|
|
||||||
.then((docs) => {
|
|
||||||
if (docs && docs.length > 0) {
|
|
||||||
for (let i = 0; i < docs.length; i++) {
|
|
||||||
const task = docs[i];
|
|
||||||
const _schedule = task.schedule && task.schedule.split(/ +/);
|
|
||||||
if (
|
|
||||||
_schedule &&
|
|
||||||
_schedule.length > 5 &&
|
|
||||||
task.status !== CrontabStatus.disabled &&
|
|
||||||
!task.isDisabled &&
|
|
||||||
task.schedule
|
|
||||||
) {
|
|
||||||
schedule.scheduleJob(task.schedule, function () {
|
|
||||||
let command = task.command as string;
|
|
||||||
if (!command.includes('task ') && !command.includes('ql ')) {
|
|
||||||
command = `task ${command}`;
|
|
||||||
}
|
|
||||||
exec(`ID=${task.id} ${command}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
Logger.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
app
|
|
||||||
.listen(config.cronPort, async () => {
|
|
||||||
await require('./loaders/sentry').default({ expressApp: app });
|
|
||||||
await require('./loaders/db').default();
|
|
||||||
|
|
||||||
await run();
|
|
||||||
Logger.debug('定时任务服务启动成功!');
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
Logger.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { AddCronRequest, AddCronResponse } from '../protos/cron';
|
||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { scheduleStacks } from './data';
|
||||||
|
import { runCron } from '../shared/runCron';
|
||||||
|
|
||||||
|
const addCron = (
|
||||||
|
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
|
||||||
|
callback: sendUnaryData<AddCronResponse>,
|
||||||
|
) => {
|
||||||
|
for (const item of call.request.crons) {
|
||||||
|
const { id, schedule, command } = item;
|
||||||
|
if (scheduleStacks.has(id)) {
|
||||||
|
scheduleStacks.get(id)?.cancel();
|
||||||
|
}
|
||||||
|
scheduleStacks.set(
|
||||||
|
id,
|
||||||
|
nodeSchedule.scheduleJob(id, schedule, async () => {
|
||||||
|
runCron(`ID=${id} ${command}`)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { addCron };
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { credentials } from '@grpc/grpc-js';
|
||||||
|
import {
|
||||||
|
AddCronRequest,
|
||||||
|
AddCronResponse,
|
||||||
|
CronClient,
|
||||||
|
DeleteCronRequest,
|
||||||
|
DeleteCronResponse,
|
||||||
|
} from '../protos/cron';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
private client = new CronClient(
|
||||||
|
`localhost:${config.cronPort}`,
|
||||||
|
credentials.createInsecure(),
|
||||||
|
);
|
||||||
|
|
||||||
|
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.addCron({ crons: request }, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.delCron({ ids: request }, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Client();
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { ToadScheduler } from 'toad-scheduler';
|
||||||
|
|
||||||
|
export const scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||||
|
|
||||||
|
export const intervalSchedule = new ToadScheduler();
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
|
||||||
|
import { scheduleStacks } from './data';
|
||||||
|
|
||||||
|
const delCron = (
|
||||||
|
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
|
||||||
|
callback: sendUnaryData<DeleteCronResponse>,
|
||||||
|
) => {
|
||||||
|
for (const id of call.request.ids) {
|
||||||
|
if (scheduleStacks.has(id)) {
|
||||||
|
scheduleStacks.get(id)?.cancel();
|
||||||
|
scheduleStacks.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { delCron };
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
|
||||||
|
import config from '../config';
|
||||||
|
import { promiseExec } from '../config/util';
|
||||||
|
|
||||||
|
const check = async (
|
||||||
|
call: ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,
|
||||||
|
callback: sendUnaryData<HealthCheckResponse>,
|
||||||
|
) => {
|
||||||
|
switch (call.request.service) {
|
||||||
|
case 'cron':
|
||||||
|
const res = await promiseExec(
|
||||||
|
`curl -s http://localhost:${config.port}/api/system`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.includes('200')) {
|
||||||
|
return callback(null, { status: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelErrLog = await promiseExec(
|
||||||
|
`tail -n 300 ~/.pm2/logs/panel-error.log`,
|
||||||
|
);
|
||||||
|
const scheduleErrLog = await promiseExec(
|
||||||
|
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
|
||||||
|
);
|
||||||
|
return callback(
|
||||||
|
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return callback(null, { status: 1 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { check };
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Server, ServerCredentials } from '@grpc/grpc-js';
|
||||||
|
import { CronService } from '../protos/cron';
|
||||||
|
import { addCron } from './addCron';
|
||||||
|
import { delCron } from './delCron';
|
||||||
|
import { HealthService } from '../protos/health';
|
||||||
|
import { check } from './health';
|
||||||
|
import config from '../config';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
const server = new Server();
|
||||||
|
server.addService(HealthService, { check });
|
||||||
|
server.addService(CronService, { addCron, delCron });
|
||||||
|
server.bindAsync(
|
||||||
|
`localhost:${config.cronPort}`,
|
||||||
|
ServerCredentials.createInsecure(),
|
||||||
|
(err, port) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
server.start();
|
||||||
|
Logger.debug(`✌️ 定时服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
|
},
|
||||||
|
);
|
||||||
+122
-159
@@ -2,16 +2,22 @@ import { Service, Inject } from 'typedi';
|
|||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
import { exec, execSync, spawn } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import fs from 'fs';
|
import 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, FindOptions } 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';
|
||||||
import { LOG_END_SYMBOL } from '../config/const';
|
import cronClient from '../schedule/client';
|
||||||
|
import { runWithCpuLimit } from '../shared/pLimit';
|
||||||
|
import { spawn } from 'cross-spawn';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
@@ -29,7 +35,12 @@ 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));
|
if (this.isSixCron(doc)) {
|
||||||
|
await cronClient.addCron([
|
||||||
|
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await this.set_crontab();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,9 +49,26 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Crontab): Promise<Crontab> {
|
public async update(payload: Crontab): Promise<Crontab> {
|
||||||
payload.saved = false;
|
const doc = await this.getDb({ id: payload.id });
|
||||||
const newDoc = await this.updateDb(payload);
|
const tab = new Crontab({ ...doc, ...payload });
|
||||||
await this.set_crontab(this.isSixCron(newDoc));
|
tab.saved = false;
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
if (doc.isDisabled === 1) {
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
if (this.isSixCron(doc)) {
|
||||||
|
await cronClient.delCron([String(newDoc.id)]);
|
||||||
|
}
|
||||||
|
if (this.isSixCron(newDoc)) {
|
||||||
|
await cronClient.addCron([
|
||||||
|
{
|
||||||
|
id: String(newDoc.id),
|
||||||
|
schedule: newDoc.schedule!,
|
||||||
|
command: newDoc.command,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await this.set_crontab();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +107,8 @@ 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 cronClient.delCron(ids.map(String));
|
||||||
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pin(ids: number[]) {
|
public async pin(ids: number[]) {
|
||||||
@@ -116,8 +145,9 @@ 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) {
|
||||||
if (!query[Op.and]) {
|
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
|
||||||
query[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;
|
||||||
@@ -160,13 +190,23 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
if (operate && operate2) {
|
if (operate && operate2) {
|
||||||
q[property] = {
|
q[property] = {
|
||||||
|
[Op.or]: [
|
||||||
|
{
|
||||||
[operate2]: [
|
[operate2]: [
|
||||||
{ [operate]: `%${value}%` },
|
{ [operate]: `%${value}%` },
|
||||||
{ [operate]: `%${encodeURIComponent(value)}%` },
|
{ [operate]: `%${encodeURI(value)}%` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[operate2]: [
|
||||||
|
where(colFn(property), operate, `%${value}%`),
|
||||||
|
where(colFn(property), operate, `%${encodeURI(value)}%`),
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
query[Op.and].push(q);
|
query[primaryOperate].push(q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,7 +227,7 @@ export default class CronService {
|
|||||||
q[column] = {
|
q[column] = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${textArray[1]}%` },
|
{ [Op.like]: `%${textArray[1]}%` },
|
||||||
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
{ [Op.like]: `%${encodeURI(textArray[1])}%` },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@@ -195,7 +235,7 @@ export default class CronService {
|
|||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchText}%` },
|
{ [Op.like]: `%${searchText}%` },
|
||||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
{ [Op.like]: `%${encodeURI(searchText)}%` },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
q[Op.or] = [
|
q[Op.or] = [
|
||||||
@@ -226,8 +266,15 @@ export default class CronService {
|
|||||||
const filterKeys: any = Object.keys(filterQuery);
|
const filterKeys: any = Object.keys(filterQuery);
|
||||||
for (const key of filterKeys) {
|
for (const key of filterKeys) {
|
||||||
let q: any = {};
|
let q: any = {};
|
||||||
if (filterKeys[key]) {
|
if (!filterQuery[key]) continue;
|
||||||
q[key] = filterKeys[key];
|
if (key === 'status') {
|
||||||
|
if (filterQuery[key].includes(2)) {
|
||||||
|
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
|
||||||
|
} else {
|
||||||
|
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
q[key] = filterQuery[key];
|
||||||
}
|
}
|
||||||
query[Op.and].push(q);
|
query[Op.and].push(q);
|
||||||
}
|
}
|
||||||
@@ -242,6 +289,19 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async find({
|
||||||
|
log_path,
|
||||||
|
}: {
|
||||||
|
log_path: string;
|
||||||
|
}): Promise<Crontab | null> {
|
||||||
|
try {
|
||||||
|
const result = await CrontabModel.findOne({ where: { log_path } });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async crontabs(params?: {
|
public async crontabs(params?: {
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
page: string;
|
page: string;
|
||||||
@@ -293,7 +353,7 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<Crontab> {
|
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {
|
||||||
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as Crontab);
|
return doc && (doc.get({ plain: true }) as Crontab);
|
||||||
}
|
}
|
||||||
@@ -303,10 +363,9 @@ export default class CronService {
|
|||||||
{ status: CrontabStatus.queued },
|
{ status: CrontabStatus.queued },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
concurrentRun(
|
ids.forEach((id) => {
|
||||||
ids.map((id) => async () => await this.runSingle(id)),
|
this.runSingle(id);
|
||||||
10,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(ids: number[]) {
|
public async stop(ids: number[]) {
|
||||||
@@ -314,31 +373,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}${LOG_END_SYMBOL}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
@@ -347,43 +386,8 @@ 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 runWithCpuLimit(() => {
|
||||||
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 });
|
||||||
if (cron.status !== CrontabStatus.queued) {
|
if (cron.status !== CrontabStatus.queued) {
|
||||||
@@ -400,8 +404,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') ||
|
||||||
@@ -443,16 +447,27 @@ export default class CronService {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 cronClient.delCron(ids.map(String));
|
||||||
|
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);
|
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||||
|
const sixCron = docs
|
||||||
|
.filter((x) => this.isSixCron(x))
|
||||||
|
.map((doc) => ({
|
||||||
|
id: String(doc.id),
|
||||||
|
schedule: doc.schedule!,
|
||||||
|
command: doc.command,
|
||||||
|
}));
|
||||||
|
await cronClient.addCron(sixCron);
|
||||||
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(id: number) {
|
public async log(id: number) {
|
||||||
@@ -465,35 +480,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)) {
|
||||||
@@ -505,65 +502,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(data?: { data: Crontab[]; total: number }) {
|
||||||
const tabs = await this.crontabs();
|
const tabs = data ?? (await this.crontabs());
|
||||||
var crontab_string = '';
|
var crontab_string = '';
|
||||||
tabs.data.forEach((tab) => {
|
tabs.data.forEach((tab) => {
|
||||||
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||||
@@ -585,9 +541,6 @@ 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`);
|
|
||||||
}
|
|
||||||
await CrontabModel.update({ saved: true }, { where: {} });
|
await CrontabModel.update({ saved: true }, { where: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,7 +578,17 @@ export default class CronService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public autosave_crontab() {
|
public async autosave_crontab() {
|
||||||
return this.set_crontab();
|
const tabs = await this.crontabs();
|
||||||
|
this.set_crontab(tabs);
|
||||||
|
|
||||||
|
const sixCron = tabs.data
|
||||||
|
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
|
||||||
|
.map((doc) => ({
|
||||||
|
id: String(doc.id),
|
||||||
|
schedule: doc.schedule!,
|
||||||
|
command: doc.command,
|
||||||
|
}));
|
||||||
|
await cronClient.addCron(sixCron);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
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 { initPosition } from '../data/env';
|
import {
|
||||||
|
initPosition,
|
||||||
|
maxPosition,
|
||||||
|
minPosition,
|
||||||
|
stepPosition,
|
||||||
|
} from '../data/env';
|
||||||
|
import { FindOptions } from 'sequelize';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronViewService {
|
export default class CronViewService {
|
||||||
@@ -16,6 +22,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +32,9 @@ export default class CronViewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: CrontabView): Promise<CrontabView> {
|
public async update(payload: CrontabView): Promise<CrontabView> {
|
||||||
const newDoc = await this.updateDb(payload);
|
const doc = await this.getDb({ id: payload.id })
|
||||||
|
const tab = new CrontabView({ ...doc, ...payload });
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +59,7 @@ export default class CronViewService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<CrontabView> {
|
public async getDb(query: FindOptions<CrontabView>['where']): Promise<CrontabView> {
|
||||||
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
|
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as CrontabView);
|
return doc && (doc.get({ plain: true }) as CrontabView);
|
||||||
}
|
}
|
||||||
@@ -62,6 +72,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,
|
||||||
@@ -85,8 +111,10 @@ export default class CronViewService {
|
|||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-28
@@ -9,11 +9,12 @@ import {
|
|||||||
unInstallDependenceCommandTypes,
|
unInstallDependenceCommandTypes,
|
||||||
DependenceModel,
|
DependenceModel,
|
||||||
} from '../data/dependence';
|
} from '../data/dependence';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import { Op } from 'sequelize';
|
import { FindOptions, Op } from 'sequelize';
|
||||||
import { concurrentRun } from '../config/util';
|
import { concurrentRun } from '../config/util';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { runOneByOne, runWithCpuLimit } from '../shared/pLimit';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class DependenceService {
|
export default class DependenceService {
|
||||||
@@ -24,7 +25,7 @@ export default class DependenceService {
|
|||||||
|
|
||||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
const tabs = payloads.map((x) => {
|
const tabs = payloads.map((x) => {
|
||||||
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
|
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
|
||||||
return tab;
|
return tab;
|
||||||
});
|
});
|
||||||
const docs = await this.insert(tabs);
|
const docs = await this.insert(tabs);
|
||||||
@@ -45,7 +46,7 @@ export default class DependenceService {
|
|||||||
const tab = new Dependence({
|
const tab = new Dependence({
|
||||||
...doc,
|
...doc,
|
||||||
...other,
|
...other,
|
||||||
status: DependenceStatus.installing,
|
status: DependenceStatus.queued,
|
||||||
});
|
});
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
this.installDependenceOneByOne([newDoc]);
|
this.installDependenceOneByOne([newDoc]);
|
||||||
@@ -59,7 +60,7 @@ export default class DependenceService {
|
|||||||
|
|
||||||
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
||||||
await DependenceModel.update(
|
await DependenceModel.update(
|
||||||
{ status: DependenceStatus.removing, log: [] },
|
{ status: DependenceStatus.queued, log: [] },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
@@ -78,7 +79,7 @@ export default class DependenceService {
|
|||||||
): Promise<Dependence[]> {
|
): Promise<Dependence[]> {
|
||||||
let condition = { ...query, type: DependenceTypes[type as any] };
|
let condition = { ...query, type: DependenceTypes[type as any] };
|
||||||
if (searchValue) {
|
if (searchValue) {
|
||||||
const encodeText = encodeURIComponent(searchValue);
|
const encodeText = encodeURI(searchValue);
|
||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchValue}%` },
|
{ [Op.like]: `%${searchValue}%` },
|
||||||
@@ -99,23 +100,19 @@ export default class DependenceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private installDependenceOneByOne(
|
public installDependenceOneByOne(
|
||||||
docs: Dependence[],
|
docs: Dependence[],
|
||||||
isInstall: boolean = true,
|
isInstall: boolean = true,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
) {
|
) {
|
||||||
concurrentRun(
|
docs.forEach((dep) => {
|
||||||
docs.map(
|
this.installOrUninstallDependency(dep, isInstall, force);
|
||||||
(dep) => async () =>
|
});
|
||||||
await this.installOrUninstallDependencies([dep], isInstall, force),
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
||||||
await DependenceModel.update(
|
await DependenceModel.update(
|
||||||
{ status: DependenceStatus.installing, log: [] },
|
{ status: DependenceStatus.queued, log: [] },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -132,7 +129,9 @@ export default class DependenceService {
|
|||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<Dependence> {
|
public async getDb(
|
||||||
|
query: FindOptions<Dependence>['where'],
|
||||||
|
): Promise<Dependence> {
|
||||||
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as Dependence);
|
return doc && (doc.get({ plain: true }) as Dependence);
|
||||||
}
|
}
|
||||||
@@ -143,30 +142,32 @@ export default class DependenceService {
|
|||||||
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
|
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public installOrUninstallDependencies(
|
public installOrUninstallDependency(
|
||||||
dependencies: Dependence[],
|
dependency: Dependence,
|
||||||
isInstall: boolean = true,
|
isInstall: boolean = true,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
) {
|
) {
|
||||||
|
return runOneByOne(() => {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
if (dependencies.length === 0) {
|
const depIds = [dependency.id!];
|
||||||
resolve(null);
|
const status = isInstall
|
||||||
return;
|
? DependenceStatus.installing
|
||||||
}
|
: DependenceStatus.removing;
|
||||||
|
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||||
|
|
||||||
const socketMessageType = !force
|
const socketMessageType = !force
|
||||||
? 'installDependence'
|
? 'installDependence'
|
||||||
: 'uninstallDependence';
|
: 'uninstallDependence';
|
||||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
const depName = dependency.name;
|
||||||
const depRunCommand = (
|
const depRunCommand = (
|
||||||
isInstall
|
isInstall
|
||||||
? InstallDependenceCommandTypes
|
? InstallDependenceCommandTypes
|
||||||
: unInstallDependenceCommandTypes
|
: unInstallDependenceCommandTypes
|
||||||
)[dependencies[0].type as any];
|
)[dependency.type as any];
|
||||||
const actionText = isInstall ? '安装' : '删除';
|
const actionText = isInstall ? '安装' : '删除';
|
||||||
const depIds = dependencies.map((x) => x.id) as number[];
|
|
||||||
const startTime = dayjs();
|
const startTime = dayjs();
|
||||||
|
|
||||||
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
|
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
)}\n\n`;
|
)}\n\n`;
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
@@ -176,7 +177,9 @@ export default class DependenceService {
|
|||||||
});
|
});
|
||||||
await this.updateLog(depIds, message);
|
await this.updateLog(depIds, message);
|
||||||
|
|
||||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
const cp = spawn(`${depRunCommand} ${depName}`, {
|
||||||
|
shell: '/bin/bash',
|
||||||
|
});
|
||||||
|
|
||||||
cp.stdout.on('data', async (data) => {
|
cp.stdout.on('data', async (data) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
@@ -203,7 +206,6 @@ export default class DependenceService {
|
|||||||
references: depIds,
|
references: depIds,
|
||||||
});
|
});
|
||||||
await this.updateLog(depIds, JSON.stringify(err));
|
await this.updateLog(depIds, JSON.stringify(err));
|
||||||
resolve(null);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cp.on('close', async (code) => {
|
cp.on('close', async (code) => {
|
||||||
@@ -241,5 +243,6 @@ export default class DependenceService {
|
|||||||
resolve(null);
|
resolve(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-9
@@ -12,7 +12,7 @@ import {
|
|||||||
stepPosition,
|
stepPosition,
|
||||||
} from '../data/env';
|
} from '../data/env';
|
||||||
import groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
import { Op } from 'sequelize';
|
import { FindOptions, Op } from 'sequelize';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class EnvService {
|
export default class EnvService {
|
||||||
@@ -49,7 +49,9 @@ export default class EnvService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Env): Promise<Env> {
|
public async update(payload: Env): Promise<Env> {
|
||||||
const newDoc = await this.updateDb(payload);
|
const doc = await this.getDb({ id: payload.id });
|
||||||
|
const tab = new Env({ ...doc, ...payload });
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
@@ -92,18 +94,22 @@ export default class EnvService {
|
|||||||
position: this.getPrecisionPosition(targetPosition),
|
position: this.getPrecisionPosition(targetPosition),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.checkPosition(targetPosition);
|
await this.checkPosition(targetPosition, envs[toIndex].position!);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkPosition(position: number) {
|
private async checkPosition(position: number, edge: number = 0) {
|
||||||
const precisionPosition = parseFloat(position.toPrecision(16));
|
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||||
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
|
if (
|
||||||
|
precisionPosition < minPosition ||
|
||||||
|
precisionPosition > maxPosition ||
|
||||||
|
Math.abs(precisionPosition - edge) < minPosition
|
||||||
|
) {
|
||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
let position = initPosition;
|
let position = initPosition;
|
||||||
for (const env of envs) {
|
for (const env of envs) {
|
||||||
position = position - stepPosition;
|
position = position - stepPosition;
|
||||||
await this.updateDb({ ...env, position });
|
await this.updateDb({ id: env.id, position });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +121,7 @@ export default class EnvService {
|
|||||||
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
|
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 = encodeURI(searchText);
|
||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchText}%` },
|
{ [Op.like]: `%${searchText}%` },
|
||||||
@@ -140,7 +146,6 @@ export default class EnvService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition, [
|
const result = await this.find(condition, [
|
||||||
['status', 'ASC'],
|
|
||||||
['position', 'DESC'],
|
['position', 'DESC'],
|
||||||
['createdAt', 'ASC'],
|
['createdAt', 'ASC'],
|
||||||
]);
|
]);
|
||||||
@@ -158,7 +163,7 @@ export default class EnvService {
|
|||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<Env> {
|
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
||||||
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as Env);
|
return doc && (doc.get({ plain: true }) as Env);
|
||||||
}
|
}
|
||||||
|
|||||||
+171
-36
@@ -36,7 +36,7 @@ export default class NotificationService {
|
|||||||
private content = '';
|
private content = '';
|
||||||
private params!: Omit<NotificationInfo, 'type'>;
|
private params!: Omit<NotificationInfo, 'type'>;
|
||||||
private gotOption = {
|
private gotOption = {
|
||||||
timeout: 30000,
|
timeout: 10000,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,7 +78,8 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async gotify() {
|
private async gotify() {
|
||||||
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -92,11 +93,19 @@ export default class NotificationService {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return typeof res.id === 'number';
|
if (typeof res.id === 'number') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async goCqHttpBot() {
|
private async goCqHttpBot() {
|
||||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -104,7 +113,14 @@ export default class NotificationService {
|
|||||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.retcode === 0;
|
if (res.retcode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async serverChan() {
|
private async serverChan() {
|
||||||
@@ -112,6 +128,7 @@ export default class NotificationService {
|
|||||||
const url = serverChanKey.startsWith('SCT')
|
const url = serverChanKey.startsWith('SCT')
|
||||||
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
||||||
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -119,12 +136,20 @@ export default class NotificationService {
|
|||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.errno === 0 || res.data.errno === 0;
|
if (res.errno === 0 || res.data.errno === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pushDeer() {
|
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`;
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -134,14 +159,23 @@ export default class NotificationService {
|
|||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return (
|
if (
|
||||||
res.content.result.length !== undefined && res.content.result.length > 0
|
res.content.result.length !== undefined &&
|
||||||
);
|
res.content.result.length > 0
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async chat() {
|
private async chat() {
|
||||||
const { chatUrl, chatToken } = this.params;
|
const { chatUrl, chatToken } = this.params;
|
||||||
const url = `${chatUrl}${chatToken}`;
|
const url = `${chatUrl}${chatToken}`;
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -149,26 +183,42 @@ export default class NotificationService {
|
|||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.success;
|
if (res.success) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async bark() {
|
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 = `https://api.day.app/${barkPush}`;
|
barkPush = `https://api.day.app/${barkPush}`;
|
||||||
}
|
}
|
||||||
const url = `${barkPush}/${encodeURIComponent(
|
const url = `${barkPush}/${encodeURIComponent(
|
||||||
this.title,
|
this.title,
|
||||||
)}/${encodeURIComponent(
|
)}/${encodeURIComponent(
|
||||||
this.content,
|
this.content,
|
||||||
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
|
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
|
||||||
|
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.get(url, {
|
.get(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.code === 200;
|
if (res.code === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async telegramBot() {
|
private async telegramBot() {
|
||||||
@@ -200,6 +250,7 @@ export default class NotificationService {
|
|||||||
https: httpsAgent,
|
https: httpsAgent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -208,7 +259,14 @@ export default class NotificationService {
|
|||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return !!res.ok;
|
if (res.ok) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async dingtalkBot() {
|
private async dingtalkBot() {
|
||||||
@@ -222,6 +280,7 @@ export default class NotificationService {
|
|||||||
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
||||||
}
|
}
|
||||||
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -233,12 +292,20 @@ export default class NotificationService {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.errcode === 0;
|
if (res.errcode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async weWorkBot() {
|
private async weWorkBot() {
|
||||||
const { weWorkBotKey } = this.params;
|
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
|
||||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -250,14 +317,21 @@ export default class NotificationService {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.errcode === 0;
|
if (res.errcode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async weWorkApp() {
|
private async weWorkApp() {
|
||||||
const { weWorkAppKey } = this.params;
|
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
|
||||||
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||||
weWorkAppKey.split(',');
|
weWorkAppKey.split(',');
|
||||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
|
||||||
const tokenRes: any = await got
|
const tokenRes: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -306,9 +380,10 @@ export default class NotificationService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(
|
.post(
|
||||||
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
||||||
{
|
{
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
@@ -321,7 +396,14 @@ export default class NotificationService {
|
|||||||
)
|
)
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
return res.errcode === 0;
|
if (res.errcode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async aibotk() {
|
private async aibotk() {
|
||||||
@@ -353,6 +435,7 @@ export default class NotificationService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -361,13 +444,20 @@ export default class NotificationService {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
|
if (res.code === 0) {
|
||||||
return res.code === 0;
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async iGot() {
|
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()}`;
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -376,12 +466,20 @@ export default class NotificationService {
|
|||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
return res.ret === 0;
|
if (res.ret === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pushPlus() {
|
private async pushPlus() {
|
||||||
const { pushPlusToken, pushPlusUser } = this.params;
|
const { pushPlusToken, pushPlusUser } = this.params;
|
||||||
const url = `https://www.pushplus.plus/send`;
|
const url = `https://www.pushplus.plus/send`;
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -394,13 +492,26 @@ export default class NotificationService {
|
|||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
return res.code === 200;
|
if (res.code === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async lark() {
|
private async lark() {
|
||||||
const { larkKey } = this.params;
|
let { larkKey } = this.params;
|
||||||
|
|
||||||
|
if (!larkKey.startsWith('http')) {
|
||||||
|
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const res: any = await got
|
const res: any = await got
|
||||||
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, {
|
.post(larkKey, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
msg_type: 'text',
|
msg_type: 'text',
|
||||||
@@ -409,11 +520,20 @@ export default class NotificationService {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return res.StatusCode === 0;
|
if (res.StatusCode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async email() {
|
private async email() {
|
||||||
const { emailPass, emailService, emailUser } = this.params;
|
const { emailPass, emailService, emailUser } = this.params;
|
||||||
|
|
||||||
|
try {
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
service: emailService,
|
service: emailService,
|
||||||
auth: {
|
auth: {
|
||||||
@@ -431,7 +551,14 @@ export default class NotificationService {
|
|||||||
|
|
||||||
transporter.close();
|
transporter.close();
|
||||||
|
|
||||||
return !!info.messageId;
|
if (info.messageId) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(info));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async webhook() {
|
private async webhook() {
|
||||||
@@ -460,8 +587,16 @@ export default class NotificationService {
|
|||||||
allowGetBody: true,
|
allowGetBody: true,
|
||||||
...bodyParam,
|
...bodyParam,
|
||||||
};
|
};
|
||||||
|
try {
|
||||||
const res = await got(formatUrl, options);
|
const res = await got(formatUrl, options);
|
||||||
return String(res.statusCode).startsWith('20');
|
if (String(res.statusCode).startsWith('20')) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatBody(contentType: string, body: any): object {
|
private formatBody(contentType: string, body: any): object {
|
||||||
@@ -484,11 +619,11 @@ export default class NotificationService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
formatUrl: url
|
formatUrl: url
|
||||||
.replaceAll('$title', encodeURIComponent(this.title))
|
?.replaceAll('$title', encodeURIComponent(this.title))
|
||||||
.replaceAll('$content', encodeURIComponent(this.content)),
|
?.replaceAll('$content', encodeURIComponent(this.content)),
|
||||||
formatBody: body
|
formatBody: body
|
||||||
.replaceAll('$title', this.title)
|
?.replaceAll('$title', this.title)
|
||||||
.replaceAll('$content', this.content),
|
?.replaceAll('$content', this.content),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default class OpenService {
|
|||||||
): Promise<App[]> {
|
): Promise<App[]> {
|
||||||
let condition = { ...query };
|
let condition = { ...query };
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const encodeText = encodeURIComponent(searchText);
|
const encodeText = encodeURI(searchText);
|
||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchText}%` },
|
{ [Op.like]: `%${searchText}%` },
|
||||||
@@ -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 },
|
||||||
|
|||||||
+26
-18
@@ -1,14 +1,16 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import nodeSchedule from 'node-schedule';
|
import nodeSchedule from 'node-schedule';
|
||||||
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
|
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||||
import {
|
import {
|
||||||
ToadScheduler,
|
ToadScheduler,
|
||||||
LongIntervalJob,
|
LongIntervalJob,
|
||||||
AsyncTask,
|
|
||||||
SimpleIntervalSchedule,
|
SimpleIntervalSchedule,
|
||||||
|
Task,
|
||||||
} from 'toad-scheduler';
|
} from 'toad-scheduler';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { runWithCpuLimit } from '../shared/pLimit';
|
||||||
|
import { spawn } from 'cross-spawn';
|
||||||
|
|
||||||
interface ScheduleTaskType {
|
interface ScheduleTaskType {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -42,7 +44,12 @@ 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 runWithCpuLimit(() => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const startTime = dayjs();
|
const startTime = dayjs();
|
||||||
@@ -50,16 +57,16 @@ export default class ScheduleService {
|
|||||||
|
|
||||||
const cp = spawn(command, { shell: '/bin/bash' });
|
const cp = spawn(command, { shell: '/bin/bash' });
|
||||||
|
|
||||||
// TODO:
|
|
||||||
callbacks.onStart?.(cp, startTime);
|
callbacks.onStart?.(cp, startTime);
|
||||||
|
completionTime === 'start' && resolve(cp.pid);
|
||||||
|
|
||||||
cp.stdout.on('data', async (data) => {
|
cp.stdout.on('data', async (data) => {
|
||||||
await callbacks.onLog?.(data.toString());
|
await callbacks.onLog?.(data.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
cp.stderr.on('data', async (data) => {
|
cp.stderr.on('data', async (data) => {
|
||||||
this.logger.error(
|
this.logger.info(
|
||||||
'执行任务 %s 失败,时间:%s, 错误信息:%j',
|
'[执行任务失败] %s,时间:%s, 错误信息:%j',
|
||||||
command,
|
command,
|
||||||
new Date().toLocaleString(),
|
new Date().toLocaleString(),
|
||||||
data.toString(),
|
data.toString(),
|
||||||
@@ -69,7 +76,7 @@ export default class ScheduleService {
|
|||||||
|
|
||||||
cp.on('error', async (err) => {
|
cp.on('error', async (err) => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'创建任务 %s 失败,时间:%s, 错误信息:%j',
|
'[创建任务失败] %s,时间:%s, 错误信息:%j',
|
||||||
command,
|
command,
|
||||||
new Date().toLocaleString(),
|
new Date().toLocaleString(),
|
||||||
err,
|
err,
|
||||||
@@ -79,7 +86,7 @@ export default class ScheduleService {
|
|||||||
|
|
||||||
cp.on('exit', async (code, signal) => {
|
cp.on('exit', async (code, signal) => {
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
`[任务退出] ${command} 进程id: ${cp.pid},退出码 ${code}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,9 +107,9 @@ export default class ScheduleService {
|
|||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
await callbacks.onError?.(JSON.stringify(error));
|
await callbacks.onError?.(JSON.stringify(error));
|
||||||
resolve(null);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCronTask(
|
async createCronTask(
|
||||||
@@ -122,19 +129,22 @@ export default class ScheduleService {
|
|||||||
this.scheduleStacks.set(
|
this.scheduleStacks.set(
|
||||||
_id,
|
_id,
|
||||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||||
await this.runTask(command, callbacks);
|
this.runTask(command, callbacks);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (runImmediately) {
|
if (runImmediately) {
|
||||||
await this.runTask(command, callbacks);
|
this.runTask(command, callbacks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info('[取消定时任务],任务名:%s', name);
|
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||||
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
if (this.scheduleStacks.has(_id)) {
|
||||||
|
this.scheduleStacks.get(_id)?.cancel();
|
||||||
|
this.scheduleStacks.delete(_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createIntervalTask(
|
async createIntervalTask(
|
||||||
@@ -150,12 +160,10 @@ export default class ScheduleService {
|
|||||||
name,
|
name,
|
||||||
command,
|
command,
|
||||||
);
|
);
|
||||||
const task = new AsyncTask(
|
const task = new Task(
|
||||||
name,
|
name,
|
||||||
async () => {
|
() => {
|
||||||
return new Promise(async (resolve, reject) => {
|
this.runTask(command, callbacks);
|
||||||
await this.runTask(command, callbacks);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@@ -176,7 +184,7 @@ export default class ScheduleService {
|
|||||||
this.intervalSchedule.addIntervalJob(job);
|
this.intervalSchedule.addIntervalJob(job);
|
||||||
|
|
||||||
if (runImmediately) {
|
if (runImmediately) {
|
||||||
await this.runTask(command, callbacks);
|
this.runTask(command, callbacks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-14
@@ -6,7 +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 { LOG_END_SYMBOL } from '../config/const';
|
import { TASK_COMMAND } from '../config/const';
|
||||||
|
import { getPid, killTask } from '../config/util';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ScriptService {
|
export default class ScriptService {
|
||||||
@@ -41,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:')}${LOG_END_SYMBOL}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-39
@@ -1,20 +1,42 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import fs from 'fs';
|
import fs, { existsSync } from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { Subscription } from '../data/subscription';
|
||||||
|
import { formatUrl } from '../config/subscription';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SshKeyService {
|
export default class SshKeyService {
|
||||||
private homedir = os.homedir();
|
private homedir = os.homedir();
|
||||||
private sshPath = path.resolve(this.homedir, '.ssh');
|
private sshPath = config.sshdPath;
|
||||||
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
|
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');
|
||||||
|
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {
|
||||||
|
this.initSshConfigFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initSshConfigFile() {
|
||||||
|
let config = '';
|
||||||
|
if (existsSync(this.sshConfigFilePath)) {
|
||||||
|
config = fs.readFileSync(this.sshConfigFilePath, { encoding: 'utf-8' });
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(this.sshConfigFilePath, '');
|
||||||
|
}
|
||||||
|
if (!config.includes(this.sshConfigHeader)) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
this.sshConfigFilePath,
|
||||||
|
`${this.sshConfigHeader}\n\n${config}`,
|
||||||
|
{ encoding: 'utf-8' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private generatePrivateKeyFile(alias: string, key: string): void {
|
private generatePrivateKeyFile(alias: string, key: string): void {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
|
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
mode: '400',
|
mode: '400',
|
||||||
});
|
});
|
||||||
@@ -23,62 +45,72 @@ export default class SshKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConfigRegx(alias: string) {
|
|
||||||
return new RegExp(
|
|
||||||
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
|
|
||||||
'g',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private removePrivateKeyFile(alias: string): void {
|
private removePrivateKeyFile(alias: string): void {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(`${this.sshPath}/${alias}`);
|
const filePath = path.join(this.sshPath, alias);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('删除私钥文件失败', error);
|
this.logger.error('删除私钥文件失败', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateSingleSshConfig(alias: string, host: string): string {
|
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
|
||||||
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
|
if (host === 'github.com') {
|
||||||
|
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
|
||||||
}
|
}
|
||||||
|
const proxyStr = proxy
|
||||||
private generateSshConfig(configs: string[]) {
|
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
|
||||||
try {
|
: '';
|
||||||
for (const config of configs) {
|
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
|
||||||
fs.appendFileSync(this.sshConfigFilePath, config, {
|
this.sshPath,
|
||||||
|
alias,
|
||||||
|
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
||||||
|
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('写入ssh配置文件失败', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeSshConfig(alias: string) {
|
private removeSshConfig(alias: string) {
|
||||||
try {
|
try {
|
||||||
const configRegx = this.getConfigRegx(alias);
|
const filePath = path.join(this.sshPath, `${alias}.config`);
|
||||||
const data = fs
|
if (existsSync(filePath)) {
|
||||||
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
|
fs.unlinkSync(filePath);
|
||||||
.replace(configRegx, '')
|
}
|
||||||
.replace(/\n[\n]+/g, '\n');
|
|
||||||
fs.writeFileSync(this.sshConfigFilePath, data, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`删除ssh配置文件${alias}失败`, error);
|
this.logger.error(`删除ssh配置文件${alias}失败`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addSSHKey(key: string, alias: string, host: string): void {
|
public addSSHKey(
|
||||||
|
key: string,
|
||||||
|
alias: string,
|
||||||
|
host: string,
|
||||||
|
proxy?: string,
|
||||||
|
): void {
|
||||||
this.generatePrivateKeyFile(alias, key);
|
this.generatePrivateKeyFile(alias, key);
|
||||||
const config = this.generateSingleSshConfig(alias, host);
|
this.generateSingleSshConfig(alias, host, proxy);
|
||||||
this.removeSshConfig(alias);
|
|
||||||
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);
|
this.removeSshConfig(alias);
|
||||||
this.removeSshConfig(config);
|
}
|
||||||
|
|
||||||
|
public setSshConfig(docs: Subscription[]) {
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
|
||||||
|
const { alias, proxy } = doc;
|
||||||
|
const { host } = formatUrl(doc);
|
||||||
|
this.removePrivateKeyFile(alias);
|
||||||
|
this.removeSshConfig(alias);
|
||||||
|
this.generatePrivateKeyFile(
|
||||||
|
alias,
|
||||||
|
(doc.pull_option as any).private_key,
|
||||||
|
);
|
||||||
|
this.generateSingleSshConfig(alias, host, proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-139
@@ -6,23 +6,19 @@ import {
|
|||||||
SubscriptionModel,
|
SubscriptionModel,
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
} from '../data/subscription';
|
} from '../data/subscription';
|
||||||
import {
|
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||||
ChildProcessWithoutNullStreams,
|
|
||||||
exec,
|
|
||||||
execSync,
|
|
||||||
spawn,
|
|
||||||
} 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,
|
||||||
|
handleLogPath,
|
||||||
|
promiseExec,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import { promises, existsSync } from 'fs';
|
import { promises, existsSync } from 'fs';
|
||||||
import { promisify } from 'util';
|
import { FindOptions, 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';
|
||||||
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
||||||
@@ -30,6 +26,7 @@ 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';
|
import { LOG_END_SYMBOL } from '../config/const';
|
||||||
|
import { formatCommand, formatUrl } from '../config/subscription';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionService {
|
export default class SubscriptionService {
|
||||||
@@ -46,7 +43,7 @@ export default class SubscriptionService {
|
|||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchText}%` },
|
{ [Op.like]: `%${searchText}%` },
|
||||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
{ [Op.like]: `%${encodeURI(searchText)}%` },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
query = {
|
query = {
|
||||||
@@ -68,62 +65,20 @@ export default class SubscriptionService {
|
|||||||
['createdAt', 'DESC'],
|
['createdAt', 'DESC'],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return result as any;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatCommand(doc: Subscription, url?: string) {
|
|
||||||
let command = 'ql ';
|
|
||||||
let _url = url || this.formatUrl(doc).url;
|
|
||||||
const { type, whitelist, blacklist, dependences, branch, extensions } = doc;
|
|
||||||
if (type === 'file') {
|
|
||||||
command += `raw "${_url}"`;
|
|
||||||
} else {
|
|
||||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
|
||||||
dependences || ''
|
|
||||||
}" "${branch || ''}" "${extensions || ''}"`;
|
|
||||||
}
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatUrl(doc: Subscription) {
|
|
||||||
let url = doc.url;
|
|
||||||
let host = '';
|
|
||||||
if (doc.type === 'private-repo') {
|
|
||||||
if (doc.pull_type === 'ssh-key') {
|
|
||||||
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
|
|
||||||
url = doc.url!.replace(host, doc.alias);
|
|
||||||
} else {
|
|
||||||
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
|
|
||||||
const { username, password } = doc.pull_option as any;
|
|
||||||
url = doc.url!.replace(host, `${username}:${password}@${host}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { url, host };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleTask(
|
public async handleTask(
|
||||||
doc: Subscription,
|
doc: Subscription,
|
||||||
needCreate = true,
|
needCreate = true,
|
||||||
needAddKey = true,
|
|
||||||
runImmediately = false,
|
runImmediately = false,
|
||||||
) {
|
) {
|
||||||
const { url, host } = this.formatUrl(doc);
|
const { url } = formatUrl(doc);
|
||||||
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
|
|
||||||
if (needAddKey) {
|
|
||||||
this.sshKeyService.addSSHKey(
|
|
||||||
(doc.pull_option as any).private_key,
|
|
||||||
doc.alias,
|
|
||||||
host,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.sshKeyService.removeSSHKey(doc.alias, host);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.command = this.formatCommand(doc, url as string);
|
doc.command = formatCommand(doc, url as string);
|
||||||
|
|
||||||
if (doc.schedule_type === 'crontab') {
|
if (doc.schedule_type === 'crontab') {
|
||||||
this.scheduleService.cancelCronTask(doc as any);
|
this.scheduleService.cancelCronTask(doc as any);
|
||||||
@@ -146,28 +101,9 @@ export default class SubscriptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async promiseExec(command: string): Promise<string> {
|
public async setSshConfig() {
|
||||||
return new Promise((resolve, reject) => {
|
const docs = await SubscriptionModel.findAll();
|
||||||
exec(
|
this.sshKeyService.setSshConfig(docs);
|
||||||
command,
|
|
||||||
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
|
|
||||||
(err, stdout, stderr) => {
|
|
||||||
resolve(stdout || stderr || JSON.stringify(err));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleLogPath(
|
|
||||||
logPath: string,
|
|
||||||
data: string = '',
|
|
||||||
): Promise<string> {
|
|
||||||
const absolutePath = path.resolve(config.logPath, logPath);
|
|
||||||
const logFileExist = await fileExist(absolutePath);
|
|
||||||
if (!logFileExist) {
|
|
||||||
await createFile(absolutePath, data);
|
|
||||||
}
|
|
||||||
return absolutePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private taskCallbacks(doc: Subscription): TaskCallbacks {
|
private taskCallbacks(doc: Subscription): TaskCallbacks {
|
||||||
@@ -182,7 +118,7 @@ export default class SubscriptionService {
|
|||||||
},
|
},
|
||||||
{ where: { id: doc.id } },
|
{ where: { id: doc.id } },
|
||||||
);
|
);
|
||||||
const absolutePath = await this.handleLogPath(
|
const absolutePath = await handleLogPath(
|
||||||
logPath as string,
|
logPath as string,
|
||||||
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
|
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
|
||||||
);
|
);
|
||||||
@@ -192,7 +128,7 @@ export default class SubscriptionService {
|
|||||||
try {
|
try {
|
||||||
if (doc.sub_before) {
|
if (doc.sub_before) {
|
||||||
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
|
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
|
||||||
beforeStr = await this.promiseExec(doc.sub_before);
|
beforeStr = await promiseExec(doc.sub_before);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
beforeStr =
|
beforeStr =
|
||||||
@@ -212,14 +148,14 @@ export default class SubscriptionService {
|
|||||||
},
|
},
|
||||||
onEnd: async (cp, endTime, diff) => {
|
onEnd: async (cp, endTime, diff) => {
|
||||||
const sub = await this.getDb({ id: doc.id });
|
const sub = await this.getDb({ id: doc.id });
|
||||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||||
|
|
||||||
// 执行 sub_after
|
// 执行 sub_after
|
||||||
let afterStr = '';
|
let afterStr = '';
|
||||||
try {
|
try {
|
||||||
if (sub.sub_after) {
|
if (sub.sub_after) {
|
||||||
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
|
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
|
||||||
afterStr = await this.promiseExec(sub.sub_after);
|
afterStr = await promiseExec(sub.sub_after);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
afterStr =
|
afterStr =
|
||||||
@@ -249,12 +185,12 @@ export default class SubscriptionService {
|
|||||||
},
|
},
|
||||||
onError: async (message: string) => {
|
onError: async (message: string) => {
|
||||||
const sub = await this.getDb({ id: doc.id });
|
const sub = await this.getDb({ id: doc.id });
|
||||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||||
},
|
},
|
||||||
onLog: async (message: string) => {
|
onLog: async (message: string) => {
|
||||||
const sub = await this.getDb({ id: doc.id });
|
const sub = await this.getDb({ id: doc.id });
|
||||||
const absolutePath = await this.handleLogPath(sub.log_path as string);
|
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -264,6 +200,7 @@ export default class SubscriptionService {
|
|||||||
const tab = new Subscription(payload);
|
const tab = new Subscription(payload);
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
await this.handleTask(doc);
|
await this.handleTask(doc);
|
||||||
|
await this.setSshConfig();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,8 +209,11 @@ 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 doc = await this.getDb({ id: payload.id });
|
||||||
|
const tab = new Subscription({ ...doc, ...payload });
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
await this.handleTask(newDoc, !newDoc.is_disabled);
|
await this.handleTask(newDoc, !newDoc.is_disabled);
|
||||||
|
await this.setSshConfig();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,12 +256,15 @@ export default class SubscriptionService {
|
|||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await this.handleTask(doc, false, false);
|
await this.handleTask(doc, false);
|
||||||
}
|
}
|
||||||
await SubscriptionModel.destroy({ where: { id: ids } });
|
await SubscriptionModel.destroy({ where: { id: ids } });
|
||||||
|
await this.setSshConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<Subscription> {
|
public async getDb(
|
||||||
|
query: FindOptions<Subscription>['where'],
|
||||||
|
): Promise<Subscription> {
|
||||||
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
|
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as Subscription);
|
return doc && (doc.get({ plain: true }) as Subscription);
|
||||||
}
|
}
|
||||||
@@ -331,10 +274,9 @@ export default class SubscriptionService {
|
|||||||
{ status: SubscriptionStatus.queued },
|
{ status: SubscriptionStatus.queued },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
concurrentRun(
|
ids.forEach((id) => {
|
||||||
ids.map((id) => async () => await this.runSingle(id)),
|
this.runSingle(id);
|
||||||
10,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(ids: number[]) {
|
public async stop(ids: number[]) {
|
||||||
@@ -342,19 +284,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 absolutePath = await handleLogPath(doc.log_path as string);
|
||||||
const err = await this.killTask(command);
|
|
||||||
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')}${LOG_END_SYMBOL}`,
|
`\n## 执行结束... ${dayjs().format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)}${LOG_END_SYMBOL}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,69 +305,33 @@ export default class SubscriptionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async killTask(name: string) {
|
|
||||||
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
try {
|
|
||||||
let pid = (await execAsync(taskCommand)).stdout;
|
|
||||||
if (pid) {
|
|
||||||
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let pids = pid.match(/\(\d+/g);
|
|
||||||
const killLogs = [];
|
|
||||||
if (pids && pids.length > 0) {
|
|
||||||
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
|
|
||||||
for (const id of pids) {
|
|
||||||
const c = `kill -9 ${id.slice(1)}`;
|
|
||||||
try {
|
|
||||||
const { stdout, stderr } = await execAsync(c);
|
|
||||||
if (stderr) {
|
|
||||||
killLogs.push(stderr);
|
|
||||||
}
|
|
||||||
if (stdout) {
|
|
||||||
killLogs.push(stdout);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
killLogs.push(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
|
||||||
} catch (e) {
|
|
||||||
return JSON.stringify(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runSingle(subscriptionId: number) {
|
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) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = this.formatCommand(subscription);
|
const command = formatCommand(subscription);
|
||||||
|
|
||||||
await this.scheduleService.runTask(
|
this.scheduleService.runTask(command, this.taskCallbacks(subscription));
|
||||||
command,
|
|
||||||
this.taskCallbacks(subscription),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
public async disabled(ids: number[]) {
|
||||||
|
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
await this.setSshConfig();
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await this.handleTask(doc, false);
|
await this.handleTask(doc, false);
|
||||||
}
|
}
|
||||||
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
public async enabled(ids: number[]) {
|
||||||
|
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
|
await this.setSshConfig();
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await this.handleTask(doc);
|
await this.handleTask(doc);
|
||||||
}
|
}
|
||||||
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(id: number) {
|
public async log(id: number) {
|
||||||
@@ -435,7 +340,7 @@ export default class SubscriptionService {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
const absolutePath = await handleLogPath(doc.log_path as string);
|
||||||
return getFileContentByName(absolutePath);
|
return getFileContentByName(absolutePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+63
-19
@@ -5,10 +5,17 @@ import * as fs from 'fs';
|
|||||||
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';
|
||||||
import ScheduleService from './schedule';
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
|
import {
|
||||||
|
getPid,
|
||||||
|
killTask,
|
||||||
|
parseContentVersion,
|
||||||
|
parseVersion,
|
||||||
|
} from '../config/util';
|
||||||
|
import { TASK_COMMAND } from '../config/const';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SystemService {
|
export default class SystemService {
|
||||||
@@ -78,31 +85,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(
|
||||||
|
`${config.lastVersionFile}?t=${Date.now()}`,
|
||||||
|
{
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
},
|
||||||
const lastVersionFileContent = result.body;
|
);
|
||||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
lastVersionContent = await parseContentVersion(result.body);
|
||||||
lastLog = lastVersionFileContent.match(logRegx)
|
|
||||||
? 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) {
|
||||||
@@ -167,4 +176,39 @@ export default class SystemService {
|
|||||||
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
|
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async run(
|
||||||
|
{ command, logPath }: { command: string; logPath: string },
|
||||||
|
callback: TaskCallbacks,
|
||||||
|
) {
|
||||||
|
if (!command.startsWith(TASK_COMMAND)) {
|
||||||
|
command = `${TASK_COMMAND} ${command}`;
|
||||||
|
}
|
||||||
|
this.scheduleService.runTask(
|
||||||
|
`real_log_path=${logPath} real_time=true ${command}`,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop({ command, pid }: { command: string; pid: number }) {
|
||||||
|
if (!pid && !command) {
|
||||||
|
return { code: 400, message: '参数错误' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid) {
|
||||||
|
await killTask(pid);
|
||||||
|
return { code: 200 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!command.startsWith(TASK_COMMAND)) {
|
||||||
|
command = `${TASK_COMMAND} ${command}`;
|
||||||
|
}
|
||||||
|
const _pid = await getPid(command);
|
||||||
|
if (_pid) {
|
||||||
|
await killTask(_pid);
|
||||||
|
return { code: 200 };
|
||||||
|
} else {
|
||||||
|
return { code: 400, message: '任务未找到' };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-12
@@ -1,6 +1,11 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
import {
|
||||||
|
createRandomString,
|
||||||
|
fileExist,
|
||||||
|
getNetIp,
|
||||||
|
getPlatform,
|
||||||
|
} from '../config/util';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
@@ -10,7 +15,6 @@ import { NotificationInfo } from '../data/notify';
|
|||||||
import NotificationService from './notify';
|
import NotificationService from './notify';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import ScheduleService from './schedule';
|
import ScheduleService from './schedule';
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@@ -205,17 +209,16 @@ export default class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private initAuthInfo() {
|
private initAuthInfo() {
|
||||||
const newPassword = createRandomString(16, 22);
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
config.authConfigFile,
|
config.authConfigFile,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
password: newPassword,
|
password: 'admin',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
code: 100,
|
code: 100,
|
||||||
message: '已初始化密码,请前往auth.json查看并重新登录',
|
message: '未找到认证文件,重新初始化',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,13 +243,18 @@ export default class UserService {
|
|||||||
return { code: 200, data: avatar, message: '更新成功' };
|
return { code: 200, data: avatar, message: '更新成功' };
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserInfo(): Promise<any> {
|
public async getUserInfo(): Promise<any> {
|
||||||
return new Promise((resolve) => {
|
const authFileExist = await fileExist(config.authConfigFile);
|
||||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
if (!authFileExist) {
|
||||||
if (err) console.log(err);
|
fs.writeFileSync(
|
||||||
resolve(JSON.parse(data));
|
config.authConfigFile,
|
||||||
});
|
JSON.stringify({
|
||||||
});
|
username: 'admin',
|
||||||
|
password: 'admin',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.getAuthInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public initTwoFactor() {
|
public initTwoFactor() {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import pLimit from "p-limit";
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
const cpuLimit = pLimit(os.cpus().length);
|
||||||
|
const oneLimit = pLimit(1);
|
||||||
|
|
||||||
|
export function runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
return cpuLimit(() => {
|
||||||
|
return fn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runOneByOne<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
return oneLimit(() => {
|
||||||
|
return fn();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { spawn } from 'cross-spawn';
|
||||||
|
import { runWithCpuLimit } from "./pLimit";
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
export function runCron(cmd: string): Promise<number> {
|
||||||
|
return runWithCpuLimit(() => {
|
||||||
|
return new Promise(async (resolve: any) => {
|
||||||
|
Logger.silly('运行命令: ' + cmd);
|
||||||
|
|
||||||
|
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
cp.stderr.on('data', (data) => {
|
||||||
|
Logger.info(
|
||||||
|
'[执行任务失败] %s,时间:%s, 错误信息:%j',
|
||||||
|
cmd,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
data.toString(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
Logger.error(
|
||||||
|
'[创建任务失败] %s,时间:%s, 错误信息:%j',
|
||||||
|
cmd,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('close', async (code) => {
|
||||||
|
Logger.info(
|
||||||
|
`[任务退出] ${cmd} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
+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();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-14
@@ -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@8.3.1 \
|
||||||
|
&& cd /tmp/build \
|
||||||
|
&& pnpm --registry https://registry.npmmirror.com install --prod
|
||||||
|
|
||||||
|
FROM python:3.10-alpine
|
||||||
|
|
||||||
ARG QL_MAINTAINER="whyour"
|
ARG QL_MAINTAINER="whyour"
|
||||||
LABEL maintainer="${QL_MAINTAINER}"
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
@@ -7,21 +16,19 @@ ARG QL_BRANCH=develop
|
|||||||
|
|
||||||
ENV PNPM_HOME=/root/.local/share/pnpm \
|
ENV PNPM_HOME=/root/.local/share/pnpm \
|
||||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
|
||||||
LANG=zh_CN.UTF-8 \
|
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
|
||||||
|
LANG=C.UTF-8 \
|
||||||
SHELL=/bin/bash \
|
SHELL=/bin/bash \
|
||||||
PS1="\u@\h:\w \$ " \
|
PS1="\u@\h:\w \$ " \
|
||||||
QL_DIR=/ql \
|
QL_DIR=/ql \
|
||||||
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 \
|
||||||
&& apk upgrade \
|
&& apk upgrade \
|
||||||
&& apk --no-cache add -f bash \
|
&& apk --no-cache add -f bash \
|
||||||
coreutils \
|
coreutils \
|
||||||
moreutils \
|
|
||||||
git \
|
git \
|
||||||
curl \
|
curl \
|
||||||
wget \
|
wget \
|
||||||
@@ -32,6 +39,8 @@ RUN set -x \
|
|||||||
nodejs \
|
nodejs \
|
||||||
jq \
|
jq \
|
||||||
openssh \
|
openssh \
|
||||||
|
procps \
|
||||||
|
netcat-openbsd \
|
||||||
npm \
|
npm \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& apk update \
|
&& apk update \
|
||||||
@@ -40,21 +49,30 @@ RUN set -x \
|
|||||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||||
&& git config --global user.name "qinglong" \
|
&& git config --global user.name "qinglong" \
|
||||||
&& git config --global http.postBuffer 524288000 \
|
&& git config --global http.postBuffer 524288000 \
|
||||||
&& npm install -g pnpm \
|
&& npm install -g pnpm@8.3.1 \
|
||||||
&& pnpm add -g pm2 ts-node typescript tslib \
|
&& cd && pnpm config set registry https://registry.npmmirror.com \
|
||||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
&& pnpm add -g pm2 tsx \
|
||||||
|
&& rm -rf /root/.pnpm-store \
|
||||||
|
&& rm -rf /root/.local/share/pnpm/store \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& rm -rf /root/.npm
|
||||||
|
|
||||||
|
ARG SOURCE_COMMIT
|
||||||
|
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||||
&& cd ${QL_DIR} \
|
&& 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 \
|
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||||
&& rm -rf /root/.pnpm-store \
|
|
||||||
&& rm -rf /root/.local/share/pnpm/store \
|
|
||||||
&& rm -rf /root/.cache \
|
|
||||||
&& rm -rf /root/.npm \
|
|
||||||
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
|
||||||
&& mkdir -p ${QL_DIR}/static \
|
&& 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}
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||||
|
CMD curl -sf http://127.0.0.1:5400/api/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
version: '2'
|
version: '2'
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
|
# alpine 基础镜像版本
|
||||||
image: whyour/qinglong:latest
|
image: whyour/qinglong:latest
|
||||||
|
# debian-slim 基础镜像版本
|
||||||
|
# image: whyour/qinglong:debian
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/ql/data
|
- ./data:/ql/data
|
||||||
ports:
|
ports:
|
||||||
- "0.0.0.0:5700:5700"
|
- "0.0.0.0:5700:5700"
|
||||||
|
environment:
|
||||||
|
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||||
|
QlBaseUrl: '/'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
+19
-25
@@ -9,15 +9,22 @@ 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
|
||||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
init_nginx
|
||||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
fix_config
|
||||||
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 -e "======================2. 安装依赖========================\n"
|
echo -e "======================2. 安装依赖========================\n"
|
||||||
|
patch_version
|
||||||
|
if [[ $PipMirror ]]; then
|
||||||
|
pip3 config set global.index-url $PipMirror
|
||||||
|
fi
|
||||||
|
current_npm_registry=$(cd && pnpm config get registry)
|
||||||
|
is_equal_registry=$(echo $current_npm_registry | grep "${NpmMirror}")
|
||||||
|
if [[ "$is_equal_registry" == "" ]]; then
|
||||||
|
cd && pnpm config set registry $NpmMirror
|
||||||
|
pnpm install -g
|
||||||
|
fi
|
||||||
update_depend
|
update_depend
|
||||||
echo
|
echo
|
||||||
|
|
||||||
@@ -25,36 +32,23 @@ echo -e "======================3. 启动nginx========================\n"
|
|||||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||||
echo -e "nginx启动成功...\n"
|
echo -e "nginx启动成功...\n"
|
||||||
|
|
||||||
echo -e "======================4. 启动面板监控========================\n"
|
echo -e "======================4. 启动pm2服务========================\n"
|
||||||
pm2 delete public &>/dev/null
|
reload_pm2
|
||||||
pm2 start $dir_static/build/public.js -n public --source-map-support --time
|
|
||||||
echo -e "监控服务启动成功...\n"
|
|
||||||
|
|
||||||
echo -e "======================5. 启动控制面板========================\n"
|
|
||||||
pm2 delete panel &>/dev/null
|
|
||||||
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
|
|
||||||
echo -e "控制面板启动成功...\n"
|
|
||||||
|
|
||||||
echo -e "======================6. 启动定时任务========================\n"
|
|
||||||
pm2 delete schedule &>/dev/null
|
|
||||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
|
|
||||||
echo -e "定时任务启动成功...\n"
|
|
||||||
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
echo -e "======================7. 启动bot========================\n"
|
echo -e "======================5. 启动bot========================\n"
|
||||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
nohup ql -l bot >$dir_log/bot.log 2>&1 &
|
||||||
echo -e "bot后台启动中...\n"
|
echo -e "bot后台启动中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EnableExtraShell == true ]]; then
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
echo -e "======================8. 执行自定义脚本========================\n"
|
echo -e "====================6. 执行自定义脚本========================\n"
|
||||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
nohup ql -l extra >$dir_log/extra.log 2>&1 &
|
||||||
echo -e "自定义脚本后台执行中...\n"
|
echo -e "自定义脚本后台执行中...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "############################################################\n"
|
echo -e "############################################################\n"
|
||||||
echo -e "容器启动成功..."
|
echo -e "容器启动成功..."
|
||||||
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
|
|
||||||
echo -e "############################################################\n"
|
echo -e "############################################################\n"
|
||||||
|
|
||||||
crond -f >/dev/null
|
crond -f >/dev/null
|
||||||
|
|||||||
+12
-7
@@ -13,31 +13,34 @@ map $http_upgrade $connection_upgrade {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 5700;
|
listen 5700;
|
||||||
root /ql/static/dist;
|
IPV6_CONFIG
|
||||||
ssl_session_timeout 5m;
|
ssl_session_timeout 5m;
|
||||||
|
|
||||||
location QL_BASE_URL/api/public/ {
|
location QL_BASE_URLapi/public/ {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass http://publicApi/api/public/;
|
proxy_pass http://publicApi/api/;
|
||||||
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location QL_BASE_URL/api/ {
|
location QL_BASE_URLapi/ {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass http://baseApi/api/;
|
proxy_pass http://baseApi/api/;
|
||||||
|
proxy_buffering off;
|
||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
location QL_BASE_URL/open/ {
|
location QL_BASE_URLopen/ {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass http://baseApi/open/;
|
proxy_pass http://baseApi/open/;
|
||||||
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
@@ -48,10 +51,12 @@ server {
|
|||||||
gzip_comp_level 6;
|
gzip_comp_level 6;
|
||||||
gzip_buffers 16 8k;
|
gzip_buffers 16 8k;
|
||||||
gzip_http_version 1.0;
|
gzip_http_version 1.0;
|
||||||
|
QL_ROOT_CONFIG
|
||||||
|
|
||||||
location QL_BASE_URL/ {
|
location QL_BASE_URL_LOCATION {
|
||||||
|
QL_ALIAS_CONFIG
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
try_files $uri $uri/ QL_BASE_URL/index.html;
|
try_files $uri QL_BASE_URLindex.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ .*\.(html)$ {
|
location ~ .*\.(html)$ {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'schedule',
|
||||||
|
max_restarts: 10,
|
||||||
|
kill_timeout: 15000,
|
||||||
|
wait_ready: true,
|
||||||
|
listen_timeout: 10000,
|
||||||
|
source_map_support: true,
|
||||||
|
time: true,
|
||||||
|
script: 'static/build/schedule/index.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'public',
|
||||||
|
max_restarts: 10,
|
||||||
|
kill_timeout: 15000,
|
||||||
|
wait_ready: true,
|
||||||
|
listen_timeout: 10000,
|
||||||
|
source_map_support: true,
|
||||||
|
time: true,
|
||||||
|
script: 'static/build/public.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'panel',
|
||||||
|
max_restarts: 10,
|
||||||
|
kill_timeout: 15000,
|
||||||
|
wait_ready: true,
|
||||||
|
listen_timeout: 10000,
|
||||||
|
source_map_support: true,
|
||||||
|
time: true,
|
||||||
|
script: 'static/build/app.js',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
+30
-11
@@ -4,12 +4,14 @@
|
|||||||
"start": "concurrently -n w: npm:start:*",
|
"start": "concurrently -n w: npm:start:*",
|
||||||
"start:front": "max 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",
|
||||||
|
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts",
|
||||||
"build:front": "max 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/index.js",
|
||||||
"public": "npm run build:back && node static/build/public.js",
|
"public": "npm run build:back && node static/build/public.js",
|
||||||
|
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
|
||||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||||
"postinstall": "max setup 2>/dev/null || true",
|
"postinstall": "max setup 2>/dev/null || true",
|
||||||
"test": "umi-test",
|
"test": "umi-test",
|
||||||
@@ -42,7 +44,8 @@
|
|||||||
"monaco-editor",
|
"monaco-editor",
|
||||||
"rc-field-form",
|
"rc-field-form",
|
||||||
"@types/lodash.merge",
|
"@types/lodash.merge",
|
||||||
"rollup"
|
"rollup",
|
||||||
|
"styled-components"
|
||||||
],
|
],
|
||||||
"allowedVersions": {
|
"allowedVersions": {
|
||||||
"react": "18",
|
"react": "18",
|
||||||
@@ -52,6 +55,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.8.13",
|
||||||
"@otplib/preset-default": "^12.0.1",
|
"@otplib/preset-default": "^12.0.1",
|
||||||
"@sentry/node": "^7.12.1",
|
"@sentry/node": "^7.12.1",
|
||||||
"@sentry/tracing": "^7.12.1",
|
"@sentry/tracing": "^7.12.1",
|
||||||
@@ -60,27 +64,33 @@
|
|||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron-parser": "^4.2.1",
|
"cron-parser": "^4.2.1",
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
"dayjs": "^1.11.2",
|
"dayjs": "^1.11.2",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"express": "^4.17.3",
|
"express": "^4.17.3",
|
||||||
"express-jwt": "^6.1.1",
|
"express-jwt": "^6.1.1",
|
||||||
|
"express-rate-limit": "^6.7.0",
|
||||||
"express-urlrewrite": "^1.4.0",
|
"express-urlrewrite": "^1.4.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
"hpagent": "^0.1.2",
|
"hpagent": "^0.1.2",
|
||||||
|
"http-proxy-middleware": "^2.0.6",
|
||||||
"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",
|
"p-limit": "3.1.0",
|
||||||
|
"protobufjs": "^7.2.3",
|
||||||
|
"pstree.remy": "^1.1.8",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sequelize": "^6.25.3",
|
"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.3",
|
||||||
"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",
|
||||||
@@ -89,13 +99,16 @@
|
|||||||
},
|
},
|
||||||
"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.2.1",
|
"@monaco-editor/react": "4.2.1",
|
||||||
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@sentry/react": "^7.12.1",
|
"@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/cross-spawn": "^6.0.2",
|
||||||
"@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.185",
|
"@types/lodash": "^4.14.185",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
@@ -105,20 +118,22 @@
|
|||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/qrcode.react": "^1.0.2",
|
"@types/qrcode.react": "^1.0.2",
|
||||||
"@types/react": "^18.0.20",
|
"@types/react": "^18.0.20",
|
||||||
|
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@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/max": "^4.0.21",
|
"@umijs/max": "^4.0.55",
|
||||||
"@umijs/ssr-darkreader": "^4.9.45",
|
"@umijs/ssr-darkreader": "^4.9.45",
|
||||||
"ansi-to-react": "^6.1.6",
|
"ansi-to-react": "^6.1.6",
|
||||||
"antd": "^4.23.0",
|
"antd": "^4.24.8",
|
||||||
"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",
|
||||||
@@ -127,6 +142,7 @@
|
|||||||
"rc-tween-one": "^3.0.6",
|
"rc-tween-one": "^3.0.6",
|
||||||
"react": "18.2.0",
|
"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",
|
||||||
@@ -134,10 +150,13 @@
|
|||||||
"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",
|
||||||
|
"ts-proto": "^1.146.0",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "^4.6.2",
|
"tsx": "^3.12.3",
|
||||||
|
"typescript": "4.8.4",
|
||||||
"umi-request": "^1.4.0",
|
"umi-request": "^1.4.0",
|
||||||
"vh-check": "^2.0.5",
|
"vh-check": "^2.0.5",
|
||||||
|
"virtualizedtableforantd4": "1.3.0",
|
||||||
"webpack": "^5.70.0",
|
"webpack": "^5.70.0",
|
||||||
"yorkie": "^2.0.0"
|
"yorkie": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+15444
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.8 KiB |
+16
-12
@@ -1,9 +1,3 @@
|
|||||||
## Version: v2.8.0
|
|
||||||
## Date: 2021-06-20
|
|
||||||
## Update Content: 可持续发展纲要\n1. session管理破坏性修改\n2. 配置管理可编辑config下文件\n3. 自定义脚本改为查看脚本\n4. 移除互助相关
|
|
||||||
|
|
||||||
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
|
|
||||||
|
|
||||||
## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务
|
## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务
|
||||||
AutoDelCron="true"
|
AutoDelCron="true"
|
||||||
|
|
||||||
@@ -16,7 +10,7 @@ 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%
|
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
|
||||||
@@ -24,11 +18,8 @@ CpuWarn=80
|
|||||||
MemoryWarn=80
|
MemoryWarn=80
|
||||||
DiskWarn=90
|
DiskWarn=90
|
||||||
|
|
||||||
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
## 设置定时任务执行的超时时间,例如1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||||
CommandTimeoutTime="1h"
|
CommandTimeoutTime=""
|
||||||
|
|
||||||
## 设置批量执行任务时的并发数,默认同时执行5个任务
|
|
||||||
MaxConcurrentNum="5"
|
|
||||||
|
|
||||||
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
|
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
|
||||||
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
|
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
|
||||||
@@ -160,4 +151,17 @@ export AIBOTK_TYPE=""
|
|||||||
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
|
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
|
||||||
export AIBOTK_NAME=""
|
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 变量名= 声明即可
|
||||||
|
|||||||
+104
-5
@@ -38,6 +38,7 @@ let SCKEY = '';
|
|||||||
//此处填你申请的PushDeer KEY.
|
//此处填你申请的PushDeer KEY.
|
||||||
//(环境变量名 DEER_KEY)
|
//(环境变量名 DEER_KEY)
|
||||||
let PUSHDEER_KEY = '';
|
let PUSHDEER_KEY = '';
|
||||||
|
let PUSHDEER_URL = '';
|
||||||
|
|
||||||
// =======================================Synology Chat通知设置区域===========================================
|
// =======================================Synology Chat通知设置区域===========================================
|
||||||
//此处填你申请的CHAT_URL与CHAT_TOKEN
|
//此处填你申请的CHAT_URL与CHAT_TOKEN
|
||||||
@@ -75,6 +76,10 @@ let DD_BOT_TOKEN = '';
|
|||||||
//密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串
|
//密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串
|
||||||
let DD_BOT_SECRET = '';
|
let DD_BOT_SECRET = '';
|
||||||
|
|
||||||
|
// =======================================企业微信基础设置===========================================
|
||||||
|
// 企业微信反向代理地址
|
||||||
|
//(环境变量名 QYWX_ORIGIN)
|
||||||
|
let QYWX_ORIGIN = '';
|
||||||
// =======================================企业微信机器人通知设置区域===========================================
|
// =======================================企业微信机器人通知设置区域===========================================
|
||||||
//此处填你企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa
|
//此处填你企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa
|
||||||
//(环境变量名 QYWX_KEY)
|
//(环境变量名 QYWX_KEY)
|
||||||
@@ -124,6 +129,18 @@ let AIBOTK_NAME = '';
|
|||||||
//FSKEY 飞书机器人的 FSKEY
|
//FSKEY 飞书机器人的 FSKEY
|
||||||
let 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;
|
||||||
@@ -151,6 +168,7 @@ 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) {
|
if (process.env.CHAT_URL) {
|
||||||
@@ -216,6 +234,12 @@ if (process.env.DD_BOT_TOKEN) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.QYWX_ORIGIN) {
|
||||||
|
QYWX_ORIGIN = process.env.QYWX_ORIGIN;
|
||||||
|
} else {
|
||||||
|
QYWX_ORIGIN = 'https://qyapi.weixin.qq.com';
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.QYWX_KEY) {
|
if (process.env.QYWX_KEY) {
|
||||||
QYWX_KEY = process.env.QYWX_KEY;
|
QYWX_KEY = process.env.QYWX_KEY;
|
||||||
}
|
}
|
||||||
@@ -248,6 +272,22 @@ if (process.env.AIBOTK_NAME) {
|
|||||||
if (process.env.FSKEY) {
|
if (process.env.FSKEY) {
|
||||||
FSKEY = 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;
|
||||||
|
}
|
||||||
//==========================云端环境变量的判断与接收=========================
|
//==========================云端环境变量的判断与接收=========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -266,6 +306,16 @@ async function sendNotify(
|
|||||||
) {
|
) {
|
||||||
//提供6种通知
|
//提供6种通知
|
||||||
desp += author; //增加作者信息,防止被贩卖等
|
desp += author; //增加作者信息,防止被贩卖等
|
||||||
|
|
||||||
|
// 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
|
||||||
|
let skipTitle = process.env.SKIP_PUSH_TITLE;
|
||||||
|
if (skipTitle) {
|
||||||
|
if (skipTitle.split('\n').includes(text)) {
|
||||||
|
console.info(text + '在SKIP_PUSH_TITLE环境变量内,跳过推送!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
serverNotify(text, desp), //微信server酱
|
serverNotify(text, desp), //微信server酱
|
||||||
pushPlusNotify(text, desp), //pushplus(推送加)
|
pushPlusNotify(text, desp), //pushplus(推送加)
|
||||||
@@ -285,6 +335,7 @@ async function sendNotify(
|
|||||||
PushDeerNotify(text, desp), //PushDeer
|
PushDeerNotify(text, desp), //PushDeer
|
||||||
aibotkNotify(text, desp), //智能微秘书
|
aibotkNotify(text, desp), //智能微秘书
|
||||||
fsBotNotify(text, desp), //飞书机器人
|
fsBotNotify(text, desp), //飞书机器人
|
||||||
|
smtpNotify(text, desp), //SMTP 邮件
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +464,7 @@ function PushDeerNotify(text, desp) {
|
|||||||
// 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',
|
||||||
@@ -492,7 +543,7 @@ function BarkNotify(text, desp, params = {}) {
|
|||||||
const options = {
|
const options = {
|
||||||
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
|
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
|
||||||
desp,
|
desp,
|
||||||
)}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
|
)}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
|
||||||
params,
|
params,
|
||||||
)}`,
|
)}`,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -651,7 +702,7 @@ function ddBotNotify(text, desp) {
|
|||||||
function qywxBotNotify(text, desp) {
|
function qywxBotNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const options = {
|
const options = {
|
||||||
url: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${QYWX_KEY}`,
|
url: `${QYWX_ORIGIN}/cgi-bin/webhook/send?key=${QYWX_KEY}`,
|
||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
@@ -713,7 +764,7 @@ function qywxamNotify(text, desp) {
|
|||||||
if (QYWX_AM) {
|
if (QYWX_AM) {
|
||||||
const QYWX_AM_AY = QYWX_AM.split(',');
|
const QYWX_AM_AY = QYWX_AM.split(',');
|
||||||
const options_accesstoken = {
|
const options_accesstoken = {
|
||||||
url: `https://qyapi.weixin.qq.com/cgi-bin/gettoken`,
|
url: `${QYWX_ORIGIN}/cgi-bin/gettoken`,
|
||||||
json: {
|
json: {
|
||||||
corpid: `${QYWX_AM_AY[0]}`,
|
corpid: `${QYWX_AM_AY[0]}`,
|
||||||
corpsecret: `${QYWX_AM_AY[1]}`,
|
corpsecret: `${QYWX_AM_AY[1]}`,
|
||||||
@@ -778,7 +829,7 @@ function qywxamNotify(text, desp) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
options = {
|
options = {
|
||||||
url: `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accesstoken}`,
|
url: `${QYWX_ORIGIN}/cgi-bin/message/send?access_token=${accesstoken}`,
|
||||||
json: {
|
json: {
|
||||||
touser: `${ChangeUserId(desp)}`,
|
touser: `${ChangeUserId(desp)}`,
|
||||||
agentid: `${QYWX_AM_AY[3]}`,
|
agentid: `${QYWX_AM_AY[3]}`,
|
||||||
@@ -978,6 +1029,8 @@ function aibotkNotify(text, desp) {
|
|||||||
resolve(data);
|
resolve(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1018,6 +1071,52 @@ function fsBotNotify(text, desp) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
|||||||
+122
-23
@@ -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,7 @@ 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_URL': '', # synology chat url
|
||||||
'CHAT_TOKEN': '', # synology chat token
|
'CHAT_TOKEN': '', # synology chat token
|
||||||
@@ -71,6 +76,8 @@ push_config = {
|
|||||||
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
|
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
|
||||||
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
||||||
|
|
||||||
|
'QYWX_ORIGIN': '', # 企业微信代理地址
|
||||||
|
|
||||||
'QYWX_AM': '', # 企业微信应用
|
'QYWX_AM': '', # 企业微信应用
|
||||||
|
|
||||||
'QYWX_KEY': '', # 企业微信机器人
|
'QYWX_KEY': '', # 企业微信机器人
|
||||||
@@ -85,6 +92,12 @@ push_config = {
|
|||||||
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
|
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
|
||||||
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
|
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
|
||||||
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
|
'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
|
||||||
@@ -219,7 +232,11 @@ def gotify(title:str,content:str) -> None:
|
|||||||
print("gotify 服务启动")
|
print("gotify 服务启动")
|
||||||
|
|
||||||
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
|
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
|
||||||
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
|
data = {
|
||||||
|
"title": title,
|
||||||
|
"message": content,
|
||||||
|
"priority": push_config.get("GOTIFY_PRIORITY"),
|
||||||
|
}
|
||||||
response = requests.post(url, data=data).json()
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if response.get("id"):
|
if response.get("id"):
|
||||||
@@ -258,10 +275,10 @@ def serverJ(title: str, content: str) -> None:
|
|||||||
print("serverJ 服务启动")
|
print("serverJ 服务启动")
|
||||||
|
|
||||||
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
||||||
if push_config.get("PUSH_KEY").index("SCT") != -1:
|
if push_config.get("PUSH_KEY").find("SCT") != -1:
|
||||||
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||||
else:
|
else:
|
||||||
url = f'https://sc.ftqq.com/${push_config.get("PUSH_KEY")}.send'
|
url = f'https://sc.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||||
response = requests.post(url, data=data).json()
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if response.get("errno") == 0 or response.get("code") == 0:
|
if response.get("errno") == 0 or response.get("code") == 0:
|
||||||
@@ -278,8 +295,16 @@ def pushdeer(title: str, content: str) -> None:
|
|||||||
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
|
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("PushDeer 服务启动")
|
print("PushDeer 服务启动")
|
||||||
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
|
data = {
|
||||||
url = 'https://api2.pushdeer.com/message/push'
|
"text": title,
|
||||||
|
"desp": content,
|
||||||
|
"type": "markdown",
|
||||||
|
"pushkey": push_config.get("DEER_KEY"),
|
||||||
|
}
|
||||||
|
url = "https://api2.pushdeer.com/message/push"
|
||||||
|
if push_config.get("DEER_URL"):
|
||||||
|
url = push_config.get("DEER_URL")
|
||||||
|
|
||||||
response = requests.post(url, data=data).json()
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if len(response.get("content").get("result")) > 0:
|
if len(response.get("content").get("result")) > 0:
|
||||||
@@ -296,7 +321,7 @@ def chat(title: str, content: str) -> None:
|
|||||||
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
|
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("chat 服务启动")
|
print("chat 服务启动")
|
||||||
data = 'payload=' + json.dumps({'text': title + '\n' + content})
|
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
||||||
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
|
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
|
||||||
response = requests.post(url, data=data)
|
response = requests.post(url, data=data)
|
||||||
|
|
||||||
@@ -306,7 +331,6 @@ def chat(title: str, content: str) -> None:
|
|||||||
print("Chat 推送失败!错误信息:", response)
|
print("Chat 推送失败!错误信息:", response)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def pushplus_bot(title: str, content: str) -> None:
|
def pushplus_bot(title: str, content: str) -> None:
|
||||||
"""
|
"""
|
||||||
通过 push+ 推送消息。
|
通过 push+ 推送消息。
|
||||||
@@ -331,7 +355,6 @@ def pushplus_bot(title: str, content: str) -> None:
|
|||||||
print("PUSHPLUS 推送成功!")
|
print("PUSHPLUS 推送成功!")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
url_old = "http://pushplus.hxtrip.com/send"
|
url_old = "http://pushplus.hxtrip.com/send"
|
||||||
headers["Accept"] = "application/json"
|
headers["Accept"] = "application/json"
|
||||||
response = requests.post(url=url_old, data=body, headers=headers).json()
|
response = requests.post(url=url_old, data=body, headers=headers).json()
|
||||||
@@ -402,9 +425,12 @@ class WeCom:
|
|||||||
self.CORPID = corpid
|
self.CORPID = corpid
|
||||||
self.CORPSECRET = corpsecret
|
self.CORPSECRET = corpsecret
|
||||||
self.AGENTID = agentid
|
self.AGENTID = agentid
|
||||||
|
self.ORIGIN = "https://qyapi.weixin.qq.com"
|
||||||
|
if push_config.get("QYWX_ORIGIN"):
|
||||||
|
self.ORIGIN = push_config.get("QYWX_ORIGIN")
|
||||||
|
|
||||||
def get_access_token(self):
|
def get_access_token(self):
|
||||||
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
url = f"{self.ORIGIN}/cgi-bin/gettoken"
|
||||||
values = {
|
values = {
|
||||||
"corpid": self.CORPID,
|
"corpid": self.CORPID,
|
||||||
"corpsecret": self.CORPSECRET,
|
"corpsecret": self.CORPSECRET,
|
||||||
@@ -414,10 +440,7 @@ class WeCom:
|
|||||||
return data["access_token"]
|
return data["access_token"]
|
||||||
|
|
||||||
def send_text(self, message, touser="@all"):
|
def send_text(self, message, touser="@all"):
|
||||||
send_url = (
|
send_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||||
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
|
|
||||||
+ self.get_access_token()
|
|
||||||
)
|
|
||||||
send_values = {
|
send_values = {
|
||||||
"touser": touser,
|
"touser": touser,
|
||||||
"msgtype": "text",
|
"msgtype": "text",
|
||||||
@@ -431,10 +454,7 @@ class WeCom:
|
|||||||
return respone["errmsg"]
|
return respone["errmsg"]
|
||||||
|
|
||||||
def send_mpnews(self, title, message, media_id, touser="@all"):
|
def send_mpnews(self, title, message, media_id, touser="@all"):
|
||||||
send_url = (
|
send_url = f"https://{self.HOST}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||||
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
|
|
||||||
+ self.get_access_token()
|
|
||||||
)
|
|
||||||
send_values = {
|
send_values = {
|
||||||
"touser": touser,
|
"touser": touser,
|
||||||
"msgtype": "mpnews",
|
"msgtype": "mpnews",
|
||||||
@@ -467,7 +487,11 @@ def wecom_bot(title: str, content: str) -> None:
|
|||||||
return
|
return
|
||||||
print("企业微信机器人服务启动")
|
print("企业微信机器人服务启动")
|
||||||
|
|
||||||
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
|
origin = "https://qyapi.weixin.qq.com"
|
||||||
|
if push_config.get("QYWX_ORIGIN"):
|
||||||
|
origin = push_config.get("QYWX_ORIGIN")
|
||||||
|
|
||||||
|
url = f"{origin}/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
|
||||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||||
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
@@ -529,24 +553,28 @@ 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"):
|
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取消推送")
|
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("智能微秘书 服务启动")
|
print("智能微秘书 服务启动")
|
||||||
|
|
||||||
if push_config.get("AIBOTK_TYPE") == 'room':
|
if push_config.get("AIBOTK_TYPE") == "room":
|
||||||
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
|
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
|
||||||
data = {
|
data = {
|
||||||
"apiKey": push_config.get("AIBOTK_KEY"),
|
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||||
"roomName": push_config.get("AIBOTK_NAME"),
|
"roomName": push_config.get("AIBOTK_NAME"),
|
||||||
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
|
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
|
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
|
||||||
data = {
|
data = {
|
||||||
"apiKey": push_config.get("AIBOTK_KEY"),
|
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||||
"name": push_config.get("AIBOTK_NAME"),
|
"name": push_config.get("AIBOTK_NAME"),
|
||||||
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
|
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
|
||||||
}
|
}
|
||||||
body = json.dumps(data).encode(encoding="utf-8")
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
@@ -558,6 +586,58 @@ def aibotk(title: str, content: str) -> None:
|
|||||||
print(f'智能微秘书 推送失败!{response["error"]}')
|
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:
|
||||||
"""
|
"""
|
||||||
获取一条一言。
|
获取一条一言。
|
||||||
@@ -598,8 +678,20 @@ 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"):
|
if (
|
||||||
|
push_config.get("AIBOTK_KEY")
|
||||||
|
and push_config.get("AIBOTK_TYPE")
|
||||||
|
and push_config.get("AIBOTK_NAME")
|
||||||
|
):
|
||||||
notify_function.append(aibotk)
|
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:
|
||||||
@@ -607,6 +699,13 @@ def send(title: str, content: str) -> None:
|
|||||||
print(f"{title} 推送内容为空!")
|
print(f"{title} 推送内容为空!")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
|
||||||
|
skipTitle = os.getenv("SKIP_PUSH_TITLE")
|
||||||
|
if skipTitle:
|
||||||
|
if title in re.split("\n", skipTitle):
|
||||||
|
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
|
||||||
|
return
|
||||||
|
|
||||||
hitokoto = push_config.get("HITOKOTO")
|
hitokoto = push_config.get("HITOKOTO")
|
||||||
|
|
||||||
text = one() if hitokoto else ""
|
text = one() if hitokoto else ""
|
||||||
|
|||||||
+1
-5
@@ -3,12 +3,8 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.0.0",
|
|
||||||
"download": "^8.0.0",
|
|
||||||
"http-server": "^0.12.3",
|
|
||||||
"got": "^11.5.1",
|
"got": "^11.5.1",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"nodemailer": "^6.8.0",
|
||||||
"request": "^2.88.2",
|
|
||||||
"tough-cookie": "^4.0.0",
|
"tough-cookie": "^4.0.0",
|
||||||
"tunnel": "0.0.6",
|
"tunnel": "0.0.6",
|
||||||
"ws": "^7.4.3"
|
"ws": "^7.4.3"
|
||||||
|
|||||||
+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(
|
||||||
|
|||||||
+74
-27
@@ -1,7 +1,25 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
create_token() {
|
||||||
|
local token_command="tsx ${dir_root}/back/token.ts"
|
||||||
|
local token_file="${dir_root}/static/build/token.js"
|
||||||
|
if [[ -f $token_file ]]; then
|
||||||
|
token_command="node ${token_file}"
|
||||||
|
fi
|
||||||
|
token=$(eval "$token_command")
|
||||||
|
}
|
||||||
|
|
||||||
get_token() {
|
get_token() {
|
||||||
|
if [[ -f $file_auth_token ]]; then
|
||||||
token=$(cat $file_auth_token | jq -r .value)
|
token=$(cat $file_auth_token | jq -r .value)
|
||||||
|
local expiration=$(cat $file_auth_token | jq -r .expiration)
|
||||||
|
local currentTimeStamp=$(date +%s)
|
||||||
|
if [[ $currentTimeStamp -ge $expiration ]]; then
|
||||||
|
create_token
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
create_token
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
add_cron_api() {
|
add_cron_api() {
|
||||||
@@ -10,10 +28,16 @@ add_cron_api() {
|
|||||||
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
||||||
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
||||||
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
||||||
|
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
|
||||||
else
|
else
|
||||||
local schedule=$1
|
local schedule="$1"
|
||||||
local command=$2
|
local command="$2"
|
||||||
local name=$3
|
local name="$3"
|
||||||
|
local sub_id="$4"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! $sub_id ]];then
|
||||||
|
sub_id="null"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
@@ -25,11 +49,11 @@ add_cron_api() {
|
|||||||
-H "Origin: http://0.0.0.0:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
|
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
code=$(echo $api | jq -r .code)
|
code=$(echo "$api" | jq -r .code)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code == 200 ]]; then
|
if [[ $code == 200 ]]; then
|
||||||
echo -e "$name -> 添加成功"
|
echo -e "$name -> 添加成功"
|
||||||
else
|
else
|
||||||
@@ -45,10 +69,10 @@ update_cron_api() {
|
|||||||
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
||||||
local id=$(echo "$1" | awk -F ":" '{print $4}')
|
local id=$(echo "$1" | awk -F ":" '{print $4}')
|
||||||
else
|
else
|
||||||
local schedule=$1
|
local schedule="$1"
|
||||||
local command=$2
|
local command="$2"
|
||||||
local name=$3
|
local name="$3"
|
||||||
local id=$4
|
local id="$4"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
@@ -64,8 +88,8 @@ update_cron_api() {
|
|||||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
|
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
code=$(echo $api | jq -r .code)
|
code=$(echo "$api" | jq -r .code)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code == 200 ]]; then
|
if [[ $code == 200 ]]; then
|
||||||
echo -e "$name -> 更新成功"
|
echo -e "$name -> 更新成功"
|
||||||
else
|
else
|
||||||
@@ -79,8 +103,8 @@ update_cron_command_api() {
|
|||||||
local command=$(echo "$1" | awk -F ":" '{print $1}')
|
local command=$(echo "$1" | awk -F ":" '{print $1}')
|
||||||
local id=$(echo "$1" | awk -F ":" '{print $2}')
|
local id=$(echo "$1" | awk -F ":" '{print $2}')
|
||||||
else
|
else
|
||||||
local command=$1
|
local command="$1"
|
||||||
local id=$2
|
local id="$2"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
@@ -96,8 +120,8 @@ update_cron_command_api() {
|
|||||||
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
|
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
code=$(echo $api | jq -r .code)
|
code=$(echo "$api" | jq -r .code)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code == 200 ]]; then
|
if [[ $code == 200 ]]; then
|
||||||
echo -e "$command -> 更新成功"
|
echo -e "$command -> 更新成功"
|
||||||
else
|
else
|
||||||
@@ -106,7 +130,7 @@ update_cron_command_api() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
del_cron_api() {
|
del_cron_api() {
|
||||||
local ids=$1
|
local ids="$1"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
||||||
@@ -121,8 +145,8 @@ del_cron_api() {
|
|||||||
--data-raw "[$ids]" \
|
--data-raw "[$ids]" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
code=$(echo $api | jq -r .code)
|
code=$(echo "$api" | jq -r .code)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code == 200 ]]; then
|
if [[ $code == 200 ]]; then
|
||||||
echo -e "成功"
|
echo -e "成功"
|
||||||
else
|
else
|
||||||
@@ -151,16 +175,16 @@ update_cron() {
|
|||||||
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
|
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
code=$(echo $api | jq -r .code)
|
code=$(echo "$api" | jq -r .code)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code != 200 ]]; then
|
if [[ $code != 200 ]]; then
|
||||||
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
|
echo -e "\n## 更新任务状态失败(${message})\n"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_api() {
|
notify_api() {
|
||||||
local title=$1
|
local title="$1"
|
||||||
local content=$2
|
local content="$2"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
|
||||||
@@ -175,13 +199,36 @@ notify_api() {
|
|||||||
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
|
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
code=$(echo $api | jq -r .code)
|
code=$(echo "$api" | jq -r .code)
|
||||||
message=$(echo $api | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code == 200 ]]; then
|
if [[ $code == 200 ]]; then
|
||||||
echo -e "通知发送成功"
|
echo -e "通知发送成功🎉"
|
||||||
else
|
else
|
||||||
echo -e "通知失败(${message})"
|
echo -e "通知失败(${message})"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
find_cron_api() {
|
||||||
|
local params="$1"
|
||||||
|
local currentTimeStamp=$(date +%s)
|
||||||
|
local api=$(
|
||||||
|
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||||
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
|
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||||
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
|
--compressed
|
||||||
|
)
|
||||||
|
data=$(echo "$api" | jq -r .data)
|
||||||
|
if [[ $data == 'null' ]]; then
|
||||||
|
echo -e ""
|
||||||
|
else
|
||||||
|
name=$(echo "$api" | jq -r .data.name)
|
||||||
|
echo -e "$name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
get_token
|
get_token
|
||||||
|
|||||||
Regular → Executable
+1
-5
@@ -1,9 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 导入通用变量与函数
|
|
||||||
dir_shell=$QL_DIR/shell
|
|
||||||
. $dir_shell/share.sh
|
|
||||||
|
|
||||||
if [[ -z ${BotRepoUrl} ]]; then
|
if [[ -z ${BotRepoUrl} ]]; then
|
||||||
url="https://github.com/SuMaiKaDe/bot.git"
|
url="https://github.com/SuMaiKaDe/bot.git"
|
||||||
repo_path="${dir_repo}/dockerbot"
|
repo_path="${dir_repo}/dockerbot"
|
||||||
@@ -46,6 +42,6 @@ echo -e "\npython3依赖安装成功...\n"
|
|||||||
echo -e "4、启动bot程序...\n"
|
echo -e "4、启动bot程序...\n"
|
||||||
make_dir $dir_log/bot
|
make_dir $dir_log/bot
|
||||||
cd $dir_data
|
cd $dir_data
|
||||||
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
|
ps -eo pid,command | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
|
||||||
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
|
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
|
||||||
echo -e "bot启动成功...\n"
|
echo -e "bot启动成功...\n"
|
||||||
|
|||||||
Regular → Executable
+20
-23
@@ -1,9 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
dir_shell=$QL_DIR/shell
|
|
||||||
. $dir_shell/share.sh
|
|
||||||
. $dir_shell/api.sh
|
|
||||||
|
|
||||||
reset_env() {
|
reset_env() {
|
||||||
echo -e "---> 1. 开始检测配置文件\n"
|
echo -e "---> 1. 开始检测配置文件\n"
|
||||||
fix_config
|
fix_config
|
||||||
@@ -29,9 +25,7 @@ copy_dep() {
|
|||||||
echo -e "---> 通知文件复制完成\n"
|
echo -e "---> 通知文件复制完成\n"
|
||||||
|
|
||||||
echo -e "---> 2. 复制nginx配置文件\n"
|
echo -e "---> 2. 复制nginx配置文件\n"
|
||||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
init_nginx
|
||||||
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
|
|
||||||
echo -e "---> 配置文件复制完成\n"
|
echo -e "---> 配置文件复制完成\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,12 +33,12 @@ pm2_log() {
|
|||||||
echo -e "---> pm2日志"
|
echo -e "---> pm2日志"
|
||||||
local panelOut="/root/.pm2/logs/panel-out.log"
|
local panelOut="/root/.pm2/logs/panel-out.log"
|
||||||
local panelError="/root/.pm2/logs/panel-error.log"
|
local panelError="/root/.pm2/logs/panel-error.log"
|
||||||
tail -n 100 "$panelOut"
|
tail -n 300 "$panelOut"
|
||||||
tail -n 100 "$panelError"
|
tail -n 300 "$panelError"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_nginx() {
|
check_nginx() {
|
||||||
local nginxPid=$(ps -ef | grep nginx | grep -v grep)
|
local nginxPid=$(ps -eo pid,command | grep nginx | grep -v grep)
|
||||||
echo -e "=====> 检测nginx服务\n$nginxPid"
|
echo -e "=====> 检测nginx服务\n$nginxPid"
|
||||||
if [[ $nginxPid ]]; then
|
if [[ $nginxPid ]]; then
|
||||||
echo -e "\n=====> nginx服务正常\n"
|
echo -e "\n=====> nginx服务正常\n"
|
||||||
@@ -60,9 +54,6 @@ check_ql() {
|
|||||||
echo -e "\n=====> 检测面板\n\n$api\n"
|
echo -e "\n=====> 检测面板\n\n$api\n"
|
||||||
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
|
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
|
||||||
echo -e "=====> 面板服务启动正常\n"
|
echo -e "=====> 面板服务启动正常\n"
|
||||||
else
|
|
||||||
echo -e "=====> 面板服务异常,重置基础环境\n"
|
|
||||||
reset_env
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +61,8 @@ check_pm2() {
|
|||||||
pm2_log
|
pm2_log
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/api/system?t=$currentTimeStamp" \
|
||||||
-H 'Accept: */*' \
|
-H 'Accept: */*' \
|
||||||
-H "Authorization: Bearer $token" \
|
|
||||||
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
|
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
|
||||||
-H 'Referer: http://0.0.0.0:5700/crontab' \
|
-H 'Referer: http://0.0.0.0:5700/crontab' \
|
||||||
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
|
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
|
||||||
@@ -81,19 +71,26 @@ check_pm2() {
|
|||||||
echo -e "\n=====> 检测后台\n\n$api\n"
|
echo -e "\n=====> 检测后台\n\n$api\n"
|
||||||
if [[ $api =~ "{\"code\"" ]]; then
|
if [[ $api =~ "{\"code\"" ]]; then
|
||||||
echo -e "=====> 后台服务启动正常\n"
|
echo -e "=====> 后台服务启动正常\n"
|
||||||
else
|
|
||||||
echo -e "=====> 后台服务异常,重置基础环境并重启后台\n"
|
|
||||||
reset_env
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
echo -e "=====> 开始检测"
|
echo -e "=====> 开始检测"
|
||||||
npm i -g pnpm
|
npm i -g pnpm@8.3.1
|
||||||
pnpm setup &>/dev/null
|
patch_version
|
||||||
source ~/.bashrc
|
|
||||||
pnpm install -g &>/dev/null
|
apk add procps netcat-openbsd
|
||||||
pnpm add -g pm2
|
|
||||||
|
if [[ $PipMirror ]]; then
|
||||||
|
pip3 config set global.index-url $PipMirror
|
||||||
|
fi
|
||||||
|
if [[ $NpmMirror ]]; then
|
||||||
|
cd && pnpm config set registry $NpmMirror
|
||||||
|
pnpm install -g
|
||||||
|
fi
|
||||||
|
|
||||||
|
pnpm add -g pm2 tsx
|
||||||
|
reset_env
|
||||||
copy_dep
|
copy_dep
|
||||||
check_ql
|
check_ql
|
||||||
check_nginx
|
check_nginx
|
||||||
|
|||||||
Regular → Executable
+10
-44
@@ -1,10 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 导入通用变量与函数
|
|
||||||
dir_shell=$QL_DIR/shell
|
|
||||||
. $dir_shell/share.sh
|
|
||||||
. $dir_shell/api.sh
|
|
||||||
|
|
||||||
random_delay() {
|
random_delay() {
|
||||||
local random_delay_max=$RandomDelay
|
local random_delay_max=$RandomDelay
|
||||||
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
|
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
|
||||||
@@ -29,7 +24,7 @@ random_delay() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
|
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
|
||||||
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
|
echo -e "任务随机延迟 $delay_second 秒,配置文件参数 RandomDelay 置空可取消延迟 \n"
|
||||||
sleep $delay_second
|
sleep $delay_second
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -43,7 +38,7 @@ gen_array_scripts() {
|
|||||||
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
||||||
let i++
|
let i++
|
||||||
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
|
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
|
||||||
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
|
array_scripts_name[i]=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||||
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
|
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -75,55 +70,26 @@ run_nohup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_server() {
|
check_server() {
|
||||||
cpu_use=$(top -b -n 1 | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
|
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_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
|
||||||
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | 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)
|
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)
|
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
|
||||||
if [[ $cpu_use -gt $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)
|
||||||
local resource=$(top -b -n 1 | 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"
|
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")
|
|
||||||
|
|
||||||
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
|
fi
|
||||||
|
|
||||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
|
||||||
. $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))
|
|
||||||
|
|
||||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
|
||||||
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒"
|
|
||||||
echo -e "\n "
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## 正常运行单个脚本,$1:传入参数
|
## 正常运行单个脚本,$1:传入参数
|
||||||
run_normal() {
|
run_normal() {
|
||||||
local file_param=$1
|
local file_param=$1
|
||||||
if [[ $# -eq 1 ]]; then
|
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]]; then
|
||||||
random_delay "$file_param"
|
random_delay "$file_param"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -164,7 +130,7 @@ run_concurrent() {
|
|||||||
|
|
||||||
local envs=$(eval echo "\$${env_param}")
|
local envs=$(eval echo "\$${env_param}")
|
||||||
local array=($(echo $envs | sed 's/&/ /g'))
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
|
||||||
|
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
local relative_path="${file_param%/*}"
|
local relative_path="${file_param%/*}"
|
||||||
@@ -227,7 +193,7 @@ run_else() {
|
|||||||
local relative_path="${file_param%/*}"
|
local relative_path="${file_param%/*}"
|
||||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||||
cd ${relative_path}
|
cd ${relative_path}
|
||||||
file_param=${file_param/$relative_path\//}
|
file_param=${file_param/$relative_path\//.\/}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
shift
|
shift
|
||||||
|
|||||||
Regular → Executable
+2
-2
@@ -11,9 +11,9 @@ echo -e "提交master代码"
|
|||||||
git push
|
git push
|
||||||
|
|
||||||
echo -e "更新cdn文件"
|
echo -e "更新cdn文件"
|
||||||
ts-node sample/tool.ts
|
tsx sample/tool.ts
|
||||||
|
|
||||||
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*")
|
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
|
||||||
version="v$string"
|
version="v$string"
|
||||||
echo -e "当前版本$version"
|
echo -e "当前版本$version"
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -1,12 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 导入通用变量与函数
|
|
||||||
dir_shell=$QL_DIR/shell
|
|
||||||
. $dir_shell/share.sh
|
|
||||||
|
|
||||||
days=$1
|
days=$1
|
||||||
|
|
||||||
## 删除运行js脚本的旧日志
|
## 删除运行脚本的旧日志
|
||||||
remove_js_log() {
|
remove_js_log() {
|
||||||
local log_full_path_list=$(find $dir_log/ -name "*.log")
|
local log_full_path_list=$(find $dir_log/ -name "*.log")
|
||||||
local diff_time
|
local diff_time
|
||||||
@@ -18,7 +14,17 @@ remove_js_log() {
|
|||||||
else
|
else
|
||||||
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
|
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
|
||||||
fi
|
fi
|
||||||
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
|
if [[ $diff_time -gt $((${days} * 86400)) ]]; then
|
||||||
|
local log_path=$(echo "$log" | sed "s,${dir_log}/,,g")
|
||||||
|
local result=$(find_cron_api "log_path=$log_path")
|
||||||
|
echo -e "查询文件 $log_path"
|
||||||
|
if [[ -z $result ]]; then
|
||||||
|
echo -e "删除中~"
|
||||||
|
rm -vf $log
|
||||||
|
else
|
||||||
|
echo -e "正在被 $result 使用,跳过~"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-78
@@ -18,6 +18,7 @@ dir_update_log=$dir_log/update
|
|||||||
ql_static_repo=$dir_repo/static
|
ql_static_repo=$dir_repo/static
|
||||||
|
|
||||||
## 文件
|
## 文件
|
||||||
|
file_ecosystem_js=$dir_root/ecosystem.config.js
|
||||||
file_config_sample=$dir_sample/config.sample.sh
|
file_config_sample=$dir_sample/config.sample.sh
|
||||||
file_env=$dir_config/env.sh
|
file_env=$dir_config/env.sh
|
||||||
file_sharecode=$dir_config/sharecode.sh
|
file_sharecode=$dir_config/sharecode.sh
|
||||||
@@ -34,7 +35,6 @@ file_notify_js_sample=$dir_sample/notify.js
|
|||||||
file_notify_py_sample=$dir_sample/notify.py
|
file_notify_py_sample=$dir_sample/notify.py
|
||||||
file_notify_py=$dir_scripts/notify.py
|
file_notify_py=$dir_scripts/notify.py
|
||||||
file_notify_js=$dir_scripts/sendNotify.js
|
file_notify_js=$dir_scripts/sendNotify.js
|
||||||
task_error_log_path=$dir_log/task_error.log
|
|
||||||
nginx_app_conf=$dir_root/docker/front.conf
|
nginx_app_conf=$dir_root/docker/front.conf
|
||||||
nginx_conf=$dir_root/docker/nginx.conf
|
nginx_conf=$dir_root/docker/nginx.conf
|
||||||
dep_notify_py=$dir_dep/notify.py
|
dep_notify_py=$dir_dep/notify.py
|
||||||
@@ -67,8 +67,8 @@ import_config() {
|
|||||||
[[ -f $file_config_user ]] && . $file_config_user
|
[[ -f $file_config_user ]] && . $file_config_user
|
||||||
[[ -f $file_env ]] && . $file_env
|
[[ -f $file_env ]] && . $file_env
|
||||||
|
|
||||||
ql_base_url=${QlBaseUrl:-""}
|
ql_base_url=${QlBaseUrl:-"/"}
|
||||||
command_timeout_time=${CommandTimeoutTime:-"1h"}
|
command_timeout_time=${CommandTimeoutTime:-""}
|
||||||
proxy_url=${ProxyUrl:-""}
|
proxy_url=${ProxyUrl:-""}
|
||||||
file_extensions=${RepoFileExtensions:-"js py"}
|
file_extensions=${RepoFileExtensions:-"js py"}
|
||||||
current_branch=${QL_BRANCH}
|
current_branch=${QL_BRANCH}
|
||||||
@@ -79,12 +79,16 @@ import_config() {
|
|||||||
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:-80}
|
cpu_warn=${CpuWarn}
|
||||||
mem_warn=${MemoryWarn:-80}
|
mem_warn=${MemoryWarn}
|
||||||
disk_warn=${DiskWarn:-90}
|
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}"
|
||||||
@@ -94,6 +98,9 @@ set_proxy() {
|
|||||||
unset_proxy() {
|
unset_proxy() {
|
||||||
unset http_proxy
|
unset http_proxy
|
||||||
unset https_proxy
|
unset https_proxy
|
||||||
|
unset ftp_proxy
|
||||||
|
unset all_proxy
|
||||||
|
unset no_proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
make_dir() {
|
make_dir() {
|
||||||
@@ -258,28 +265,13 @@ npm_install_sub() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
npm_install_1() {
|
|
||||||
local dir_current=$(pwd)
|
|
||||||
local dir_work=$1
|
|
||||||
|
|
||||||
cd $dir_work
|
|
||||||
echo -e "运行 npm install...\n"
|
|
||||||
npm_install_sub
|
|
||||||
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
|
|
||||||
cd $dir_current
|
|
||||||
}
|
|
||||||
|
|
||||||
npm_install_2() {
|
npm_install_2() {
|
||||||
local dir_current=$(pwd)
|
local dir_current=$(pwd)
|
||||||
local dir_work=$1
|
local dir_work=$1
|
||||||
|
|
||||||
cd $dir_work
|
cd $dir_work
|
||||||
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
|
echo -e "安装 $dir_work 依赖包...\n"
|
||||||
npm_install_sub
|
npm_install_sub
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
|
|
||||||
npm_install_1 $dir_work
|
|
||||||
fi
|
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,15 +295,20 @@ 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 --depth=1 $part_cmd $url $dir
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
|
|
||||||
|
reset_branch "$branch" "$dir"
|
||||||
|
|
||||||
unset_proxy
|
unset_proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,16 +316,28 @@ 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"
|
||||||
|
set_proxy "$proxy"
|
||||||
|
|
||||||
set_proxy
|
if [[ ! $branch ]]; then
|
||||||
git fetch --all
|
branch=$(cd $dir_work && git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pre_commit_id=$(git rev-parse --short HEAD)
|
||||||
|
reset_branch "$branch" "$dir_work"
|
||||||
|
|
||||||
|
git fetch --depth 1 origin $branch
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
git pull &>/dev/null
|
|
||||||
unset_proxy
|
|
||||||
reset_branch "$branch"
|
|
||||||
|
|
||||||
|
reset_branch "$branch" "$dir_work"
|
||||||
|
local cur_commit_id=$(git rev-parse --short HEAD)
|
||||||
|
if [[ $cur_commit_id != $pre_commit_id ]]; then
|
||||||
|
exit_status=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset_proxy
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,25 +347,24 @@ 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
|
||||||
reset_branch "$branch"
|
git remote add origin $url &>/dev/null
|
||||||
cd $dir_current
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
reset_branch() {
|
reset_branch() {
|
||||||
local branch="$1"
|
local branch="$1"
|
||||||
if [[ $branch ]]; then
|
local part_cmd="origin/${branch}"
|
||||||
part_cmd="origin/${branch}"
|
git remote set-branches origin $branch
|
||||||
git checkout -B "$branch" &>/dev/null
|
|
||||||
git branch --set-upstream-to=$part_cmd $branch &>/dev/null
|
|
||||||
fi
|
|
||||||
git reset --hard $part_cmd &>/dev/null
|
git reset --hard $part_cmd &>/dev/null
|
||||||
|
git checkout -b $branch $part_cmd &>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
random_range() {
|
random_range() {
|
||||||
@@ -366,19 +374,11 @@ random_range() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reload_pm2() {
|
reload_pm2() {
|
||||||
pm2 l &>/dev/null
|
cd $dir_root
|
||||||
|
# 代理会影响 grpc 服务
|
||||||
echo -e "启动面板服务\n"
|
unset_proxy
|
||||||
pm2 delete panel --source-map-support --time &>/dev/null
|
pm2 flush &>/dev/null
|
||||||
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
|
pm2 startOrGracefulReload $file_ecosystem_js
|
||||||
|
|
||||||
echo -e "启动定时任务服务\n"
|
|
||||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
|
||||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
|
||||||
|
|
||||||
echo -e "启动公开服务\n"
|
|
||||||
pm2 delete public --source-map-support --time &>/dev/null
|
|
||||||
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
diff_time() {
|
diff_time() {
|
||||||
@@ -410,9 +410,9 @@ format_log_time() {
|
|||||||
local time="$2"
|
local time="$2"
|
||||||
|
|
||||||
if [[ $is_macos -eq 1 ]]; then
|
if [[ $is_macos -eq 1 ]]; then
|
||||||
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
|
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
|
||||||
else
|
else
|
||||||
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
|
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,17 +428,9 @@ format_timestamp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
patch_version() {
|
patch_version() {
|
||||||
if [[ $PipMirror ]]; then
|
git config --global pull.rebase false
|
||||||
pip3 config set global.index-url $PipMirror
|
|
||||||
fi
|
|
||||||
if [[ $NpmMirror ]]; then
|
|
||||||
npm config set registry $NpmMirror
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 兼容pnpm@7
|
cp -f $dir_root/.env.example $dir_root/.env
|
||||||
pnpm setup &>/dev/null
|
|
||||||
source ~/.bashrc
|
|
||||||
pnpm install -g &>/dev/null
|
|
||||||
|
|
||||||
if [[ -f "$dir_root/db/cookie.db" ]]; then
|
if [[ -f "$dir_root/db/cookie.db" ]]; then
|
||||||
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
||||||
@@ -447,14 +439,6 @@ patch_version() {
|
|||||||
echo
|
echo
|
||||||
fi
|
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
|
if [[ -d "$dir_root/db" ]]; then
|
||||||
echo -e "检测到旧的db目录,拷贝到data目录...\n"
|
echo -e "检测到旧的db目录,拷贝到data目录...\n"
|
||||||
cp -rf $dir_root/config $dir_root/data
|
cp -rf $dir_root/config $dir_root/data
|
||||||
@@ -480,10 +464,59 @@ patch_version() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init_nginx() {
|
||||||
|
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||||
|
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||||
|
local location_url="/"
|
||||||
|
local aliasStr=""
|
||||||
|
local rootStr=""
|
||||||
|
if [[ $ql_base_url != "/" ]]; then
|
||||||
|
location_url="^~${ql_base_url%*/}"
|
||||||
|
aliasStr="alias ${dir_static}/dist;"
|
||||||
|
else
|
||||||
|
rootStr="root ${dir_static}/dist;"
|
||||||
|
fi
|
||||||
|
sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf
|
||||||
|
sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf
|
||||||
|
sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf
|
||||||
|
sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf
|
||||||
|
|
||||||
|
ipv6=$(ip a | grep inet6)
|
||||||
|
ipv6Str=""
|
||||||
|
if [[ $ipv6 ]]; then
|
||||||
|
ipv6Str="listen [::]:5700 ipv6only=on;"
|
||||||
|
fi
|
||||||
|
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_task_before() {
|
||||||
|
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
|
||||||
|
echo -e "## 开始执行... $begin_time\n"
|
||||||
|
|
||||||
|
[[ $is_macos -eq 0 ]] && check_server
|
||||||
|
|
||||||
|
. $file_task_before "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_task_after() {
|
||||||
|
. $file_task_after "$@"
|
||||||
|
|
||||||
|
local etime=$(date "+$time_format")
|
||||||
|
local end_time=$(format_time "$time_format" "$etime")
|
||||||
|
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||||
|
local diff_time=$(($end_timestamp - $begin_timestamp))
|
||||||
|
|
||||||
|
[[ "$diff_time" == 0 ]] && diff_time=1
|
||||||
|
|
||||||
|
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
||||||
|
|
||||||
|
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
}
|
||||||
|
|
||||||
init_env
|
init_env
|
||||||
detect_termux
|
detect_termux
|
||||||
detect_macos
|
detect_macos
|
||||||
define_cmd
|
define_cmd
|
||||||
fix_config
|
|
||||||
|
|
||||||
import_config $1 2>$task_error_log_path
|
import_config $1
|
||||||
|
|||||||
+60
-12
@@ -5,6 +5,12 @@ dir_shell=$QL_DIR/shell
|
|||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/api.sh
|
. $dir_shell/api.sh
|
||||||
|
|
||||||
|
trap "single_hanle" 2 3 20 15 14
|
||||||
|
single_hanle() {
|
||||||
|
eval handle_task_after "$@" "$cmd"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
## 选择python3还是node
|
## 选择python3还是node
|
||||||
define_program() {
|
define_program() {
|
||||||
local file_param=$1
|
local file_param=$1
|
||||||
@@ -15,7 +21,11 @@ define_program() {
|
|||||||
elif [[ $file_param == *.sh ]]; then
|
elif [[ $file_param == *.sh ]]; then
|
||||||
which_program="bash"
|
which_program="bash"
|
||||||
elif [[ $file_param == *.ts ]]; then
|
elif [[ $file_param == *.ts ]]; then
|
||||||
|
if ! type tsx &>/dev/null; then
|
||||||
which_program="ts-node-transpile-only"
|
which_program="ts-node-transpile-only"
|
||||||
|
else
|
||||||
|
which_program="tsx"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
which_program=""
|
which_program=""
|
||||||
fi
|
fi
|
||||||
@@ -28,12 +38,20 @@ handle_log_path() {
|
|||||||
file_param="task"
|
file_param="task"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -z $ID ]]; then
|
||||||
|
ID=$(cat $list_crontab_user | grep -E "$cmd_task.* $file_param" | perl -pe "s|.*ID=(.*) $cmd_task.* $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
|
fi
|
||||||
local suffix=""
|
local suffix=""
|
||||||
if [[ ! -z $ID ]]; then
|
if [[ ! -z $ID ]]; then
|
||||||
|
if [[ "$ID" -gt 0 ]] 2>/dev/null; then
|
||||||
suffix="_${ID}"
|
suffix="_${ID}"
|
||||||
|
else
|
||||||
|
ID=""
|
||||||
fi
|
fi
|
||||||
time=$(date "+$time_format")
|
fi
|
||||||
log_time=$(format_log_time "$time_format" "$time")
|
|
||||||
|
time=$(date "+$mtime_format")
|
||||||
|
log_time=$(format_log_time "$mtime_format" "$time")
|
||||||
log_dir_tmp="${file_param##*/}"
|
log_dir_tmp="${file_param##*/}"
|
||||||
if [[ $file_param =~ "/" ]]; then
|
if [[ $file_param =~ "/" ]]; then
|
||||||
if [[ $file_param == /* ]]; then
|
if [[ $file_param == /* ]]; then
|
||||||
@@ -47,35 +65,65 @@ handle_log_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%.*}${suffix}"
|
log_dir="${log_dir_tmp%.*}${suffix}"
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
|
|
||||||
|
if [[ $real_log_path ]]; then
|
||||||
|
log_path="$real_log_path"
|
||||||
|
fi
|
||||||
|
|
||||||
cmd=">> $dir_log/$log_path 2>&1"
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
[[ "$show_log" == "true" ]] && cmd=""
|
|
||||||
make_dir "$dir_log/$log_dir"
|
make_dir "$dir_log/$log_dir"
|
||||||
|
if [[ "$show_log" == "true" ]]; then
|
||||||
|
cmd="2>&1 | tee -a $dir_log/$log_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$real_time" == "true" ]]; then
|
||||||
|
cmd=""
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
format_params() {
|
format_params() {
|
||||||
time_format="%Y-%m-%d %H:%M:%S"
|
time_format="%Y-%m-%d %H:%M:%S"
|
||||||
timeoutCmd=""
|
if [[ $is_macos -eq 1 ]]; then
|
||||||
if type timeout &>/dev/null; then
|
mtime_format=$time_format
|
||||||
timeoutCmd="timeout -k 10s $command_timeout_time "
|
else
|
||||||
|
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
|
||||||
fi
|
fi
|
||||||
params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
|
timeoutCmd=""
|
||||||
|
if [[ $command_timeout_time ]]; then
|
||||||
|
if type timeout &>/dev/null; then
|
||||||
|
timeoutCmd="timeout --foreground -s 2 -k 10s $command_timeout_time "
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
|
||||||
}
|
}
|
||||||
|
|
||||||
show_log="false"
|
init_begin_time() {
|
||||||
while getopts ":l" opt; do
|
begin_time=$(format_time "$time_format" "$time")
|
||||||
|
begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
|
}
|
||||||
|
|
||||||
|
while getopts ":lm:" opt; do
|
||||||
case $opt in
|
case $opt in
|
||||||
l)
|
l)
|
||||||
show_log="true"
|
show_log="true"
|
||||||
;;
|
;;
|
||||||
|
m)
|
||||||
|
max_time="$OPTARG"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
[[ $show_log ]] && shift $(($OPTIND - 1))
|
||||||
|
if [[ $max_time ]]; then
|
||||||
|
shift $(($OPTIND - 1))
|
||||||
|
command_timeout_time="$max_time"
|
||||||
|
fi
|
||||||
|
|
||||||
format_params "$@"
|
format_params "$@"
|
||||||
define_program "$@"
|
define_program "$@"
|
||||||
handle_log_path "$@"
|
handle_log_path "$@"
|
||||||
|
init_begin_time
|
||||||
|
|
||||||
eval . $dir_shell/otask.sh "$params" "$cmd"
|
eval . $dir_shell/otask.sh "$cmd"
|
||||||
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
|
[[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+90
-87
@@ -26,39 +26,12 @@ 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
|
||||||
local type=$2
|
local type=$2
|
||||||
if [[ -s $list ]]; then
|
if [[ -s $list ]]; then
|
||||||
echo -e "检测到有$type的定时任务:\n"
|
echo -e "检测到有${type}的定时任务:\n"
|
||||||
cat $list
|
cat $list
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
@@ -71,9 +44,9 @@ del_cron() {
|
|||||||
local path=$2
|
local path=$2
|
||||||
local detail=""
|
local detail=""
|
||||||
local ids=""
|
local ids=""
|
||||||
echo -e "开始尝试自动删除失效的定时任务...\n"
|
echo -e "开始尝试自动删除失效的定时任务..."
|
||||||
for cron in $(cat $list_drop); do
|
for cron in $(cat $list_drop); do
|
||||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
|
local id=$(cat $list_crontab_user | grep -E "$cmd_task.* $cron" | perl -pe "s|.*ID=(.*) $cmd_task.* $cron\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
if [[ $ids ]]; then
|
if [[ $ids ]]; then
|
||||||
ids="$ids,\"$id\""
|
ids="$ids,\"$id\""
|
||||||
else
|
else
|
||||||
@@ -81,7 +54,7 @@ del_cron() {
|
|||||||
fi
|
fi
|
||||||
cron_file="$dir_scripts/${cron}"
|
cron_file="$dir_scripts/${cron}"
|
||||||
if [[ -f $cron_file ]]; then
|
if [[ -f $cron_file ]]; then
|
||||||
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||||
rm -f $cron_file
|
rm -f $cron_file
|
||||||
fi
|
fi
|
||||||
[[ -z $cron_name ]] && cron_name="$cron"
|
[[ -z $cron_name ]] && cron_name="$cron"
|
||||||
@@ -102,7 +75,7 @@ del_cron() {
|
|||||||
add_cron() {
|
add_cron() {
|
||||||
local list_add=$1
|
local list_add=$1
|
||||||
local path=$2
|
local path=$2
|
||||||
echo -e "开始尝试自动添加定时任务...\n"
|
echo -e "开始尝试自动添加定时任务..."
|
||||||
local detail=""
|
local detail=""
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
for file in $(cat $list_add); do
|
for file in $(cat $list_add); do
|
||||||
@@ -119,12 +92,12 @@ add_cron() {
|
|||||||
s| | |g;
|
s| | |g;
|
||||||
}" | sort -u | head -1
|
}" | sort -u | head -1
|
||||||
)
|
)
|
||||||
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||||
[[ -z $cron_name ]] && cron_name="$file_name"
|
[[ -z $cron_name ]] && cron_name="$file_name"
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line="$default_cron"
|
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||||
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
|
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name:$SUB_ID")
|
||||||
echo -e "$result"
|
echo -e "$result"
|
||||||
if [[ $detail ]]; then
|
if [[ $detail ]]; then
|
||||||
detail="${detail}${result}\n"
|
detail="${detail}${result}\n"
|
||||||
@@ -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
|
||||||
@@ -170,24 +147,42 @@ update_repo() {
|
|||||||
|
|
||||||
## 更新所有 raw 文件
|
## 更新所有 raw 文件
|
||||||
update_raw() {
|
update_raw() {
|
||||||
echo -e "--------------------------------------------------------------\n"
|
|
||||||
local url="$1"
|
local url="$1"
|
||||||
|
local proxy="$2"
|
||||||
|
local autoAddCron="$3"
|
||||||
|
local autoDelCron="$4"
|
||||||
|
|
||||||
|
if [[ ! $autoAddCron ]]; then
|
||||||
|
autoAddCron=${AutoAddCron}
|
||||||
|
fi
|
||||||
|
if [[ ! $autoDelCron ]]; then
|
||||||
|
autoDelCron=${AutoDelCron}
|
||||||
|
fi
|
||||||
|
|
||||||
|
local proxyStr=""
|
||||||
|
if [[ $proxy ]]; then
|
||||||
|
if [[ $url == http:* ]]; then
|
||||||
|
proxyStr="-e \"http_proxy=${proxy}\""
|
||||||
|
elif [[ $url == https:* ]]; then
|
||||||
|
proxyStr="-e \"http_proxy=${proxy};https_proxy=${proxy}\""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
local raw_url="$url"
|
local raw_url="$url"
|
||||||
local suffix="${raw_url##*.}"
|
local suffix="${raw_url##*.}"
|
||||||
local raw_file_name="${uniq_path}.${suffix}"
|
local raw_file_name="${uniq_path}.${suffix}"
|
||||||
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
|
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
|
||||||
|
|
||||||
set_proxy
|
wget -q --no-check-certificate $proxyStr -O "$dir_raw/${raw_file_name}.new" ${raw_url}
|
||||||
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
|
|
||||||
unset_proxy
|
|
||||||
|
|
||||||
if [[ $? -eq 0 ]]; then
|
if [[ $? -eq 0 ]]; then
|
||||||
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
|
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
|
||||||
echo -e "下载 ${raw_file_name} 成功...\n"
|
echo -e "下载 ${raw_file_name} 成功...\n"
|
||||||
cd $dir_raw
|
cd $dir_raw
|
||||||
local filename="raw_${raw_file_name}"
|
local filename="raw_${raw_file_name}"
|
||||||
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | 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 | awk -F " " '{print $1}')
|
||||||
cp -f $raw_file_name $dir_scripts/${filename}
|
cp -f $raw_file_name $dir_scripts/${filename}
|
||||||
|
if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then
|
||||||
cron_line=$(
|
cron_line=$(
|
||||||
perl -ne "{
|
perl -ne "{
|
||||||
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
|
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
|
||||||
@@ -198,13 +193,12 @@ update_raw() {
|
|||||||
s| | |g;
|
s| | |g;
|
||||||
}" | sort -u | head -1
|
}" | sort -u | head -1
|
||||||
)
|
)
|
||||||
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||||
[[ -z $cron_name ]] && cron_name="$raw_file_name"
|
[[ -z $cron_name ]] && cron_name="$raw_file_name"
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line="$default_cron"
|
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||||
if [[ -z $cron_id ]]; then
|
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name:$SUB_ID")
|
||||||
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
|
|
||||||
echo -e "$result\n"
|
echo -e "$result\n"
|
||||||
notify_api "新增任务通知" "\n$result"
|
notify_api "新增任务通知" "\n$result"
|
||||||
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
||||||
@@ -218,14 +212,11 @@ update_raw() {
|
|||||||
|
|
||||||
## 调用用户自定义的extra.sh
|
## 调用用户自定义的extra.sh
|
||||||
run_extra_shell() {
|
run_extra_shell() {
|
||||||
if [[ ${EnableExtraShell} == true ]]; then
|
|
||||||
if [[ -f $file_extra_shell ]]; then
|
if [[ -f $file_extra_shell ]]; then
|
||||||
echo -e "--------------------------------------------------------------\n"
|
|
||||||
. $file_extra_shell
|
. $file_extra_shell
|
||||||
else
|
else
|
||||||
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
|
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## 脚本用法
|
## 脚本用法
|
||||||
@@ -244,23 +235,25 @@ usage() {
|
|||||||
|
|
||||||
## 更新qinglong
|
## 更新qinglong
|
||||||
update_qinglong() {
|
update_qinglong() {
|
||||||
patch_version &>/dev/null
|
local mirror="gitee"
|
||||||
|
local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
|
||||||
|
if [[ ! -z $githubStatus ]]; then
|
||||||
|
mirror="github"
|
||||||
|
fi
|
||||||
|
echo -e "使用 ${mirror} 源更新...\n"
|
||||||
export isFirstStartServer=false
|
export isFirstStartServer=false
|
||||||
|
|
||||||
local all_branch=$(git branch -a)
|
|
||||||
local primary_branch="master"
|
local primary_branch="master"
|
||||||
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
|
if [[ "${QL_BRANCH}" == "develop" ]]; then
|
||||||
primary_branch="${current_branch}"
|
primary_branch="develop"
|
||||||
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"
|
||||||
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)
|
||||||
@@ -275,17 +268,16 @@ update_qinglong() {
|
|||||||
update_qinglong_static() {
|
update_qinglong_static() {
|
||||||
local no_restart="$1"
|
local no_restart="$1"
|
||||||
local primary_branch="$2"
|
local primary_branch="$2"
|
||||||
local url="https://github.com/whyour/qinglong-static.git"
|
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)
|
|
||||||
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
|
||||||
@@ -309,6 +301,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"
|
||||||
|
|
||||||
@@ -318,13 +319,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
|
||||||
@@ -362,10 +363,10 @@ gen_list_repo() {
|
|||||||
done
|
done
|
||||||
files=$(eval $cmd | sed 's/^..//')
|
files=$(eval $cmd | sed 's/^..//')
|
||||||
if [[ $path ]]; then
|
if [[ $path ]]; then
|
||||||
files=$(echo "$files" | egrep $path)
|
files=$(echo "$files" | egrep "$path")
|
||||||
fi
|
fi
|
||||||
if [[ $blackword ]]; then
|
if [[ $blackword ]]; then
|
||||||
files=$(echo "$files" | egrep -v $blackword)
|
files=$(echo "$files" | egrep -v "$blackword")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
|
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
|
||||||
@@ -373,7 +374,7 @@ gen_list_repo() {
|
|||||||
|
|
||||||
if [[ $dependence ]]; then
|
if [[ $dependence ]]; then
|
||||||
cd ${repo_path}
|
cd ${repo_path}
|
||||||
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
|
results=$(eval $cmd | sed 's/^..//' | egrep "$dependence")
|
||||||
for _file in ${results}; do
|
for _file in ${results}; do
|
||||||
file_path=$(dirname $_file)
|
file_path=$(dirname $_file)
|
||||||
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
|
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
|
||||||
@@ -386,15 +387,15 @@ gen_list_repo() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
for file in ${files}; do
|
for file in ${files}; do
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,13 +426,16 @@ 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}"
|
local log_dir="${p1}"
|
||||||
make_dir "$dir_log/$log_dir"
|
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")
|
||||||
@@ -447,12 +451,9 @@ main() {
|
|||||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
|
||||||
local begin_time=$(format_time "$time_format" "$time")
|
local begin_time=$(format_time "$time_format" "$time")
|
||||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
|
||||||
|
|
||||||
if [[ -s $task_error_log_path ]]; then
|
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
|
||||||
eval cat $task_error_log_path $cmd
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
|
|
||||||
eval echo $cmd
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
|
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
|
||||||
@@ -462,6 +463,7 @@ main() {
|
|||||||
|
|
||||||
case $p1 in
|
case $p1 in
|
||||||
update)
|
update)
|
||||||
|
fix_config
|
||||||
eval update_qinglong "$2" $cmd
|
eval update_qinglong "$2" $cmd
|
||||||
;;
|
;;
|
||||||
extra)
|
extra)
|
||||||
@@ -470,7 +472,7 @@ main() {
|
|||||||
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
|
||||||
eval echo -e "命令输入错误...\\\n" $cmd
|
eval echo -e "命令输入错误...\\\n" $cmd
|
||||||
eval usage $cmd
|
eval usage $cmd
|
||||||
@@ -479,7 +481,7 @@ main() {
|
|||||||
raw)
|
raw)
|
||||||
get_uniq_path "$p2"
|
get_uniq_path "$p2"
|
||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_raw "$p2"
|
update_raw "$p2" "$p3" "$p4"
|
||||||
else
|
else
|
||||||
eval echo -e "命令输入错误...\\\n" $cmd
|
eval echo -e "命令输入错误...\\\n" $cmd
|
||||||
eval usage $cmd
|
eval usage $cmd
|
||||||
@@ -497,7 +499,7 @@ main() {
|
|||||||
resetlet)
|
resetlet)
|
||||||
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
|
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
|
||||||
echo "$auth_value" >$file_auth_user
|
echo "$auth_value" >$file_auth_user
|
||||||
echo -e "重置登录错误次数成功" $cmd
|
eval echo -e "重置登录错误次数成功" $cmd
|
||||||
;;
|
;;
|
||||||
resettfa)
|
resettfa)
|
||||||
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)
|
||||||
@@ -514,10 +516,11 @@ main() {
|
|||||||
local end_time=$(format_time "$time_format" "$etime")
|
local end_time=$(format_time "$time_format" "$etime")
|
||||||
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||||
local diff_time=$(($end_timestamp - $begin_timestamp))
|
local diff_time=$(($end_timestamp - $begin_timestamp))
|
||||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
|
||||||
|
|
||||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
eval echo -e "\\\n " $cmd
|
|
||||||
|
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
|
||||||
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒 " $cmd
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -f $file_path ]]; then
|
if [[ -f $file_path ]]; then
|
||||||
cat $file_path
|
cat $file_path
|
||||||
|
|||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
const baseUrl = window.__ENV__QlBaseUrl || '/';
|
||||||
|
|
||||||
|
export function modifyClientRenderOpts(memo: any) {
|
||||||
|
return {
|
||||||
|
...memo,
|
||||||
|
publicPath: baseUrl,
|
||||||
|
basename: baseUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modifyContextOpts(memo: any) {
|
||||||
|
return {
|
||||||
|
...memo,
|
||||||
|
basename: baseUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1,404 @@
|
|||||||
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useContext,
|
||||||
|
createContext,
|
||||||
|
useReducer,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
import { throttle, isNumber, debounce } from 'lodash';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
// 行高度
|
||||||
|
rowHeight: 0,
|
||||||
|
// 当前的scrollTop
|
||||||
|
curScrollTop: 0,
|
||||||
|
// 总行数
|
||||||
|
totalLen: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function reducer(state, action) {
|
||||||
|
const { curScrollTop, totalLen, ifScrollTopClear, scrollTop } = action;
|
||||||
|
|
||||||
|
let stateScrollTop = state.curScrollTop;
|
||||||
|
switch (action.type) {
|
||||||
|
// 改变trs 即 改变渲染的列表trs
|
||||||
|
case 'changeTrs':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
curScrollTop,
|
||||||
|
};
|
||||||
|
// 更改totalLen
|
||||||
|
case 'changeTotalLen':
|
||||||
|
if (totalLen === 0) {
|
||||||
|
stateScrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
totalLen,
|
||||||
|
curScrollTop: stateScrollTop,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'reset':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
curScrollTop: ifScrollTopClear ? 0 : scrollTop ?? state.curScrollTop,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============全局常量 ================== //
|
||||||
|
const DEFAULT_VID = 'vtable';
|
||||||
|
const vidMap = new Map();
|
||||||
|
let preData = 0;
|
||||||
|
|
||||||
|
// ===============context ============== //
|
||||||
|
const ScrollContext = createContext({
|
||||||
|
dispatch: undefined,
|
||||||
|
renderLen: 1,
|
||||||
|
start: 0,
|
||||||
|
offsetStart: 0,
|
||||||
|
// =============
|
||||||
|
rowHeight: initialState.rowHeight,
|
||||||
|
totalLen: 0,
|
||||||
|
vid: DEFAULT_VID,
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============组件 =================== //
|
||||||
|
|
||||||
|
function VCell(props: any): JSX.Element {
|
||||||
|
const { children, ...restProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td {...restProps}>
|
||||||
|
<div>{children[1]}</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VRow(props: any, ref: any): JSX.Element {
|
||||||
|
const { rowHeight } = useContext(ScrollContext);
|
||||||
|
|
||||||
|
const { children, style, ...restProps } = props;
|
||||||
|
|
||||||
|
const trRef = useRef<HTMLTableRowElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
{...restProps}
|
||||||
|
ref={Object.prototype.hasOwnProperty.call(ref, 'current') ? ref : trRef}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
height: rowHeight || 'auto',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VWrapper(props: any): JSX.Element {
|
||||||
|
const { children, ...restProps } = props;
|
||||||
|
|
||||||
|
const { renderLen, start, dispatch, vid, totalLen } =
|
||||||
|
useContext(ScrollContext);
|
||||||
|
|
||||||
|
const contents = useMemo(() => {
|
||||||
|
return children[1];
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
const contentsLen = useMemo(() => {
|
||||||
|
return contents?.length ?? 0;
|
||||||
|
}, [contents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (totalLen !== contentsLen) {
|
||||||
|
dispatch({
|
||||||
|
type: 'changeTotalLen',
|
||||||
|
totalLen: contentsLen ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [contentsLen, dispatch, vid, totalLen]);
|
||||||
|
|
||||||
|
let tempNode = null;
|
||||||
|
if (Array.isArray(contents) && contents.length) {
|
||||||
|
tempNode = [
|
||||||
|
children[0],
|
||||||
|
contents.slice(start, start + (renderLen ?? 1)).map((item) => {
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
// 兼容antd v4.3.5 --- rc-table 7.8.1及以下
|
||||||
|
return item[0];
|
||||||
|
}
|
||||||
|
// 处理antd ^v4.4.0 --- rc-table ^7.8.2
|
||||||
|
return item;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
tempNode = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <tbody {...restProps}>{tempNode}</tbody>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VTable(props: any, otherParams): JSX.Element {
|
||||||
|
const { style, children, ...rest } = props;
|
||||||
|
const { width, ...rest_style } = style;
|
||||||
|
|
||||||
|
const { vid, scrollY, resetScrollTopWhenDataChange, rowHeight, scrollTop } =
|
||||||
|
otherParams ?? {};
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
...initialState,
|
||||||
|
curScrollTop: scrollTop,
|
||||||
|
rowHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrap_tableRef = useRef<HTMLDivElement>(null);
|
||||||
|
const tableRef = useRef<HTMLTableElement>(null);
|
||||||
|
|
||||||
|
const ifChangeRef = useRef(false);
|
||||||
|
|
||||||
|
// 数据的总条数
|
||||||
|
const [totalLen, setTotalLen] = useState<number>(
|
||||||
|
children[1]?.props?.data?.length ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTotalLen(state.totalLen);
|
||||||
|
}, [state.totalLen]);
|
||||||
|
|
||||||
|
// 组件卸载的清除操作
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
vidMap.delete(vid);
|
||||||
|
};
|
||||||
|
}, [vid]);
|
||||||
|
|
||||||
|
// 数据变更
|
||||||
|
useEffect(() => {
|
||||||
|
ifChangeRef.current = true;
|
||||||
|
// console.log('数据变更')
|
||||||
|
if (isNumber(children[1]?.props?.data?.length)) {
|
||||||
|
dispatch({
|
||||||
|
type: 'changeTotalLen',
|
||||||
|
totalLen: children[1]?.props?.data?.length ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [children[1].props.data]);
|
||||||
|
|
||||||
|
// table总高度
|
||||||
|
const tableHeight = useMemo<string | number>(() => {
|
||||||
|
let temp: string | number = 'auto';
|
||||||
|
|
||||||
|
if (rowHeight && totalLen) {
|
||||||
|
temp = rowHeight * totalLen;
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
}, [totalLen]);
|
||||||
|
|
||||||
|
// table的scrollY值
|
||||||
|
const [tableScrollY, setTableScrollY] = useState(0);
|
||||||
|
|
||||||
|
// tableScrollY 随scrollY / tableHeight 进行变更
|
||||||
|
useEffect(() => {
|
||||||
|
let temp = 0;
|
||||||
|
|
||||||
|
if (typeof scrollY === 'string') {
|
||||||
|
temp =
|
||||||
|
(wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight ?? 0;
|
||||||
|
} else {
|
||||||
|
temp = scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (isNumber(tableHeight) && tableHeight < temp) {
|
||||||
|
// temp = tableHeight;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 处理tableScrollY <= 0的情况
|
||||||
|
if (temp <= 0) {
|
||||||
|
temp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTableScrollY(temp);
|
||||||
|
}, [scrollY, tableHeight]);
|
||||||
|
|
||||||
|
// 渲染的条数
|
||||||
|
const renderLen = useMemo<number>(() => {
|
||||||
|
let temp = 1;
|
||||||
|
if (rowHeight && totalLen && tableScrollY) {
|
||||||
|
if (tableScrollY <= 0) {
|
||||||
|
temp = 0;
|
||||||
|
} else {
|
||||||
|
const tempRenderLen = ((tableScrollY / rowHeight) | 0) + 10;
|
||||||
|
// console.log('tempRenderLen', tempRenderLen)
|
||||||
|
// temp = tempRenderLen > totalLen ? totalLen : tempRenderLen;
|
||||||
|
temp = tempRenderLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return temp;
|
||||||
|
}, [totalLen, tableScrollY]);
|
||||||
|
|
||||||
|
// 渲染中的第一条
|
||||||
|
let start = rowHeight ? (state.curScrollTop / rowHeight) | 0 : 0;
|
||||||
|
|
||||||
|
start = start < 5 ? 0 : start - 5 + 1;
|
||||||
|
|
||||||
|
// 偏移量
|
||||||
|
let offsetStart = state.curScrollTop % (rowHeight * 5);
|
||||||
|
|
||||||
|
if (start > 0) {
|
||||||
|
offsetStart = offsetStart % rowHeight;
|
||||||
|
}
|
||||||
|
// console.log(offsetStart)
|
||||||
|
// offsetStart= offsetStart%rowHeight
|
||||||
|
// 用来优化向上滚动出现的空白
|
||||||
|
if (state.curScrollTop && state.curScrollTop >= rowHeight * 5) {
|
||||||
|
// start -= 1
|
||||||
|
// if (offsetStart >= rowHeight) {
|
||||||
|
// offsetStart +=
|
||||||
|
// } else {
|
||||||
|
offsetStart += rowHeight * 4;
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
start = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(state.curScrollTop, start, offsetStart)
|
||||||
|
|
||||||
|
// 数据变更 操作scrollTop
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollNode = wrap_tableRef.current?.parentNode as HTMLElement;
|
||||||
|
if (ifChangeRef?.current) {
|
||||||
|
// console.log(scrollNode)
|
||||||
|
ifChangeRef.current = false;
|
||||||
|
|
||||||
|
if (resetScrollTopWhenDataChange) {
|
||||||
|
// 重置scrollTop
|
||||||
|
if (scrollNode) {
|
||||||
|
scrollNode.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: 'reset', ifScrollTopClear: true });
|
||||||
|
} else {
|
||||||
|
// console.log(preData)
|
||||||
|
// scrollNode.scrollTop = preData+53
|
||||||
|
// 不重置scrollTop 不清空curScrollTop
|
||||||
|
dispatch({ type: 'reset', ifScrollTopClear: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vidMap.has(vid)) {
|
||||||
|
vidMap.set(vid, {
|
||||||
|
...vidMap.get(vid),
|
||||||
|
scrollNode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [totalLen, resetScrollTopWhenDataChange, vid, children]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const throttleScroll = throttle((e) => {
|
||||||
|
const scrollTop: number = e?.target?.scrollTop ?? 0;
|
||||||
|
// const scrollHeight: number = e?.target?.scrollHeight ?? 0
|
||||||
|
// const clientHeight: number = e?.target?.clientHeight ?? 0
|
||||||
|
if (scrollTop) {
|
||||||
|
preData = scrollTop;
|
||||||
|
}
|
||||||
|
// 到底了 没有滚动条就不会触发reachEnd. 建议设置scrolly高度少点或者数据量多点.
|
||||||
|
// 若renderLen大于totalLen, 置空curScrollTop. => table paddingTop会置空.
|
||||||
|
dispatch({
|
||||||
|
type: 'changeTrs',
|
||||||
|
curScrollTop: renderLen <= totalLen ? scrollTop : 0,
|
||||||
|
});
|
||||||
|
}, 60);
|
||||||
|
|
||||||
|
const ref = wrap_tableRef?.current?.parentNode as HTMLElement;
|
||||||
|
|
||||||
|
if (ref) {
|
||||||
|
ref.addEventListener('scroll', throttleScroll, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ref.removeEventListener('scroll', throttleScroll);
|
||||||
|
};
|
||||||
|
}, [renderLen, totalLen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="virtuallist"
|
||||||
|
ref={wrap_tableRef}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
height: tableHeight,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
paddingTop: state.curScrollTop,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollContext.Provider
|
||||||
|
value={{
|
||||||
|
dispatch,
|
||||||
|
start,
|
||||||
|
offsetStart,
|
||||||
|
renderLen,
|
||||||
|
totalLen,
|
||||||
|
vid,
|
||||||
|
rowHeight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
{...rest}
|
||||||
|
ref={tableRef}
|
||||||
|
style={{
|
||||||
|
...rest_style,
|
||||||
|
width,
|
||||||
|
position: 'relative',
|
||||||
|
transform: `translateY(-${offsetStart}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</ScrollContext.Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================导出===================
|
||||||
|
export function VList(props: {
|
||||||
|
height: number;
|
||||||
|
// 唯一标识
|
||||||
|
vid?: string;
|
||||||
|
rowHeight: number | string;
|
||||||
|
reset?: boolean;
|
||||||
|
scrollTop?: number | string;
|
||||||
|
}): any {
|
||||||
|
const { vid = DEFAULT_VID, rowHeight, height, reset, scrollTop } = props;
|
||||||
|
|
||||||
|
const resetScrollTopWhenDataChange = reset ?? true;
|
||||||
|
|
||||||
|
if (!vidMap.has(vid)) {
|
||||||
|
vidMap.set(vid, { _id: vid });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
table: (p) =>
|
||||||
|
VTable(p, {
|
||||||
|
vid,
|
||||||
|
scrollY: height,
|
||||||
|
rowHeight,
|
||||||
|
resetScrollTopWhenDataChange,
|
||||||
|
scrollTop,
|
||||||
|
}),
|
||||||
|
body: {
|
||||||
|
wrapper: VWrapper,
|
||||||
|
row: VRow,
|
||||||
|
cell: VCell,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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>(0);
|
||||||
|
|
||||||
|
useResizeObserver(target, (entry) => {
|
||||||
|
let _target = entry.target as any;
|
||||||
|
if (!_target.classList.contains('ant-table-wrapper')) {
|
||||||
|
_target = entry.target.querySelector('.ant-table-wrapper');
|
||||||
|
}
|
||||||
|
setHeight(getTableScroll({ extraHeight, target: _target 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: {
|
||||||
@@ -93,4 +94,4 @@ export default {
|
|||||||
contentWidth: 'Fixed',
|
contentWidth: 'Fixed',
|
||||||
splitMenus: false,
|
splitMenus: false,
|
||||||
siderWidth: 180,
|
siderWidth: 180,
|
||||||
} as any;
|
} as BasicLayoutProps;
|
||||||
|
|||||||
+92
-16
@@ -9,9 +9,25 @@
|
|||||||
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
|
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
// 禁止手机页面下拉刷新
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// 禁止手机页面弹簧效果
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
height: calc(100vh - var(--vh-offset, 0px));
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-modal-body {
|
.ant-modal-body {
|
||||||
max-height: calc(85vh - 110px);
|
max-height: calc(90vh - 110px);
|
||||||
max-height: calc(85vh - var(--vh-offset, 110px));
|
max-height: calc(90vh - var(--vh-offset, 110px));
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,10 +269,11 @@ 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: none;
|
|
||||||
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: @text-color;
|
||||||
|
color: @text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
::placeholder {
|
::placeholder {
|
||||||
@@ -310,24 +327,25 @@ select:-webkit-autofill:focus {
|
|||||||
.side-menu-user-drop-menu {
|
.side-menu-user-drop-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
outline: none;
|
padding: 2px 10px;
|
||||||
padding: 4px 0;
|
|
||||||
border: 1px solid fade(@component-background, 0.12);
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: @component-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-dark='true'] .side-menu-user-drop-menu {
|
|
||||||
background-color: #373739;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider-logo {
|
.ant-pro-sider-logo {
|
||||||
padding: 16px 8px !important;
|
.title {
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
margin: 0 5px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 32px;
|
||||||
|
vertical-align: middle;
|
||||||
|
animation: pro-layout-title-hide 0.3s;
|
||||||
|
|
||||||
h1 {
|
a {
|
||||||
margin-left: 5px !important;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@@ -341,4 +359,62 @@ select:-webkit-autofill:focus {
|
|||||||
// 移动端logo被拉伸
|
// 移动端logo被拉伸
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-image {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtuallist .ant-table-tbody > tr > td > div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtuallist .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell > div {
|
||||||
|
overflow: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-bordered .virtuallist > table > .ant-table-tbody > tr > td {
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-column-title {
|
||||||
|
flex: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-column-sorters,
|
||||||
|
.ant-table-filter-column {
|
||||||
|
justify-content: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.ant-input {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pro-sider-collapsed,
|
||||||
|
body[data-mode='phone'] header {
|
||||||
|
.title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-design-pro .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
|
||||||
|
box-shadow: none;
|
||||||
|
border-right: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-31
@@ -13,9 +13,20 @@ 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';
|
||||||
@@ -32,6 +43,17 @@ export interface SharedContext {
|
|||||||
reloadUser: (needLoading?: boolean) => void;
|
reloadUser: (needLoading?: boolean) => void;
|
||||||
reloadTheme: () => void;
|
reloadTheme: () => void;
|
||||||
socketMessage: any;
|
socketMessage: any;
|
||||||
|
systemInfo: TSystemInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TSystemInfo {
|
||||||
|
branch: 'develop' | 'master';
|
||||||
|
isInitialized: boolean;
|
||||||
|
lastCommitId: string;
|
||||||
|
lastCommitTime: number;
|
||||||
|
version: string;
|
||||||
|
changeLog: string;
|
||||||
|
changeLogLink: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
@@ -40,7 +62,7 @@ export default function () {
|
|||||||
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);
|
||||||
@@ -69,14 +91,14 @@ export default function () {
|
|||||||
if (!data.isInitialized) {
|
if (!data.isInitialized) {
|
||||||
history.push('/initialization');
|
history.push('/initialization');
|
||||||
} else {
|
} else {
|
||||||
|
init(data.version);
|
||||||
getUser();
|
getUser();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
})
|
});
|
||||||
.finally(() => setInitLoading(false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUser = (needLoading = true) => {
|
const getUser = (needLoading = true) => {
|
||||||
@@ -97,6 +119,22 @@ export default function () {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getHealthStatus = () => {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}public/health`)
|
||||||
|
.then((res) => {
|
||||||
|
if (res?.data?.status === 1) {
|
||||||
|
getSystemInfo();
|
||||||
|
} else {
|
||||||
|
history.push('/error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
history.push('/error');
|
||||||
|
})
|
||||||
|
.finally(() => setInitLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
const reloadUser = (needLoading = false) => {
|
const reloadUser = (needLoading = false) => {
|
||||||
getUser(needLoading);
|
getUser(needLoading);
|
||||||
};
|
};
|
||||||
@@ -108,10 +146,8 @@ export default function () {
|
|||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!systemInfo) {
|
getHealthStatus();
|
||||||
getSystemInfo();
|
}, []);
|
||||||
}
|
|
||||||
}, [systemInfo]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (theme === 'vs-dark') {
|
if (theme === 'vs-dark') {
|
||||||
@@ -123,7 +159,6 @@ export default function () {
|
|||||||
|
|
||||||
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;
|
||||||
@@ -201,9 +236,6 @@ export default function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
|
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
|
||||||
document.title = `${
|
|
||||||
(config.documentTitleMap as any)[location.pathname]
|
|
||||||
} - 控制面板`;
|
|
||||||
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
|
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
|
||||||
history.push('/crontab');
|
history.push('/crontab');
|
||||||
}
|
}
|
||||||
@@ -230,13 +262,17 @@ export default function () {
|
|||||||
!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 />
|
||||||
) : (
|
) : (
|
||||||
@@ -244,18 +280,23 @@ export default function () {
|
|||||||
selectedKeys={[location.pathname]}
|
selectedKeys={[location.pathname]}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
ErrorBoundary={Sentry.ErrorBoundary}
|
ErrorBoundary={Sentry.ErrorBoundary}
|
||||||
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
|
logo={
|
||||||
title={
|
|
||||||
<>
|
<>
|
||||||
<span style={{ fontSize: 16 }}>控制面板</span>
|
<Image preview={false} src="https://qn.whyour.cn/logo.png" />
|
||||||
|
<div className="title">
|
||||||
|
<span className="title">青龙</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,
|
||||||
@@ -265,11 +306,15 @@ export default function () {
|
|||||||
letterSpacing: isQQBrowser ? -2 : 0,
|
letterSpacing: isQQBrowser ? -2 : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
v{version}
|
v{systemInfo?.version}
|
||||||
</span>
|
</span>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
title={false}
|
||||||
menuItemRender={(menuItemProps: any, defaultDom: any) => {
|
menuItemRender={(menuItemProps: any, defaultDom: any) => {
|
||||||
if (
|
if (
|
||||||
menuItemProps.isUrl ||
|
menuItemProps.isUrl ||
|
||||||
@@ -281,16 +326,15 @@ export default function () {
|
|||||||
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"
|
||||||
@@ -312,7 +356,7 @@ export default function () {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!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"
|
||||||
@@ -342,6 +386,7 @@ export default function () {
|
|||||||
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;
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -147,22 +148,10 @@ const CronDetailModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
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: string, s: string;
|
|
||||||
let index = cmd[1].lastIndexOf('/');
|
|
||||||
if (index >= 0) {
|
|
||||||
s = cmd[1].slice(index + 1);
|
|
||||||
p = cmd[1].slice(0, index);
|
|
||||||
} else {
|
|
||||||
s = cmd[1];
|
|
||||||
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 || ''}`)
|
||||||
@@ -171,7 +160,7 @@ const CronDetailModal = ({
|
|||||||
setValue(data);
|
setValue(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else if (result) {
|
||||||
setValidTabs([validTabs[0]]);
|
setValidTabs([validTabs[0]]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,13 +4,11 @@
|
|||||||
|
|
||||||
.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);
|
min-height: 300px;
|
||||||
height: calc(80vh - var(--vh-offset, 0px) - 238px);
|
height: calc(90vh - 367px);
|
||||||
|
height: calc(90vh - var(--vh-offset, 0px) - 367px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +156,7 @@
|
|||||||
.view-create-modal-sorts {
|
.view-create-modal-sorts {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.ant-space-item:nth-child(2) {
|
.ant-space-item:nth-child(1) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,3 +174,30 @@ 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;
|
||||||
|
width: calc(100% - 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-mode='desktop'] {
|
||||||
|
.crontab-wrapper {
|
||||||
|
tbody .ant-table-cell {
|
||||||
|
height: 69px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+192
-250
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef } 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,14 +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, useOutletContext } from '@umijs/max';
|
||||||
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 { SharedContext } from '@/layouts';
|
||||||
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
|
import { getCommandScript, parseCrontab } from '@/utils';
|
||||||
|
import { ColumnProps } from 'antd/lib/table';
|
||||||
|
import { useVT } from 'virtualizedtableforantd4';
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -82,15 +85,28 @@ enum OperationPath {
|
|||||||
'unpin',
|
'unpin',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICrontab {
|
||||||
|
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 Crontab = () => {
|
||||||
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||||
const columns: any = [
|
const columns: ColumnProps<ICrontab>[] = [
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 150,
|
width: 150,
|
||||||
align: 'center' as const,
|
|
||||||
render: (text: string, record: any) => (
|
render: (text: string, record: any) => (
|
||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
@@ -138,22 +154,20 @@ const Crontab = () => {
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
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: 300,
|
width: 300,
|
||||||
align: 'center' as const,
|
render: (text, record) => {
|
||||||
render: (text: string, record: any) => {
|
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
style={{
|
style={{
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-all',
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
textAlign: 'left',
|
|
||||||
}}
|
}}
|
||||||
ellipsis={{ tooltip: text, rows: 2 }}
|
ellipsis={{ tooltip: text, rows: 2 }}
|
||||||
>
|
>
|
||||||
@@ -176,23 +190,21 @@ const Crontab = () => {
|
|||||||
dataIndex: 'schedule',
|
dataIndex: 'schedule',
|
||||||
key: 'schedule',
|
key: 'schedule',
|
||||||
width: 110,
|
width: 110,
|
||||||
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,
|
|
||||||
dataIndex: 'last_execution_time',
|
dataIndex: 'last_execution_time',
|
||||||
key: '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
|
||||||
@@ -213,7 +225,6 @@ const Crontab = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '最后运行时长',
|
title: '最后运行时长',
|
||||||
align: 'center' as const,
|
|
||||||
width: 120,
|
width: 120,
|
||||||
dataIndex: 'last_running_time',
|
dataIndex: 'last_running_time',
|
||||||
key: 'last_running_time',
|
key: 'last_running_time',
|
||||||
@@ -222,7 +233,7 @@ const Crontab = () => {
|
|||||||
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)
|
||||||
: '-';
|
: '-';
|
||||||
@@ -230,14 +241,13 @@ const Crontab = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '下次运行时间',
|
title: '下次运行时间',
|
||||||
align: 'center' as const,
|
|
||||||
width: 150,
|
width: 150,
|
||||||
sorter: {
|
sorter: {
|
||||||
compare: (a: any, b: any) => {
|
compare: (a: any, b: 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, {
|
||||||
@@ -246,13 +256,11 @@ const Crontab = () => {
|
|||||||
.replace(' 24:', ' 00:');
|
.replace(' 24:', ' 00:');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
align: 'center' as const,
|
width: 88,
|
||||||
width: 85,
|
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
text: '运行中',
|
text: '运行中',
|
||||||
@@ -271,14 +279,7 @@ const Crontab = () => {
|
|||||||
value: 3,
|
value: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onFilter: (value: number, record: any) => {
|
render: (text, record) => (
|
||||||
if (record.isDisabled && record.status !== 0) {
|
|
||||||
return value === 2;
|
|
||||||
} else {
|
|
||||||
return record.status === value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: (text: string, record: any) => (
|
|
||||||
<>
|
<>
|
||||||
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
|
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
|
||||||
<>
|
<>
|
||||||
@@ -313,9 +314,8 @@ const Crontab = () => {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
align: 'center' as const,
|
width: 130,
|
||||||
width: 100,
|
render: (text, record, index) => {
|
||||||
render: (text: string, record: any, index: number) => {
|
|
||||||
const isPc = !isPhone;
|
const isPc = !isPhone;
|
||||||
return (
|
return (
|
||||||
<Space size="middle">
|
<Space size="middle">
|
||||||
@@ -376,7 +376,6 @@ const Crontab = () => {
|
|||||||
filters: 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('');
|
||||||
@@ -389,26 +388,15 @@ const Crontab = () => {
|
|||||||
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 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: string, s: string;
|
|
||||||
let index = cmd[1].lastIndexOf('/');
|
|
||||||
if (index >= 0) {
|
|
||||||
s = cmd[1].slice(index + 1);
|
|
||||||
p = cmd[1].slice(0, index);
|
|
||||||
} else {
|
|
||||||
s = cmd[1];
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -420,9 +408,9 @@ const Crontab = () => {
|
|||||||
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
||||||
filters,
|
filters,
|
||||||
)}`;
|
)}`;
|
||||||
if (sorter && sorter.field) {
|
if (sorter && sorter.column && sorter.order) {
|
||||||
url += `&sorter=${JSON.stringify({
|
url += `&sorter=${JSON.stringify({
|
||||||
field: sorter.field,
|
field: sorter.column.key,
|
||||||
type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
|
type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
|
||||||
})}`;
|
})}`;
|
||||||
}
|
}
|
||||||
@@ -430,6 +418,7 @@ const Crontab = () => {
|
|||||||
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
|
||||||
@@ -441,10 +430,7 @@ const Crontab = () => {
|
|||||||
data.map((x) => {
|
data.map((x) => {
|
||||||
return {
|
return {
|
||||||
...x,
|
...x,
|
||||||
nextRunTime: cron_parser
|
nextRunTime: parseCrontab(x.schedule),
|
||||||
.parseExpression(x.schedule)
|
|
||||||
.next()
|
|
||||||
.toDate(),
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -679,18 +665,15 @@ const Crontab = () => {
|
|||||||
index: number;
|
index: number;
|
||||||
}> = ({ record, index }) => (
|
}> = ({ record, index }) => (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
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 />
|
||||||
@@ -717,34 +700,15 @@ const Crontab = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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}`)
|
||||||
@@ -752,10 +716,7 @@ const Crontab = () => {
|
|||||||
if (code === 200) {
|
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.nextRunTime = cron_parser
|
data.nextRunTime = parseCrontab(data.schedule);
|
||||||
.parseExpression(data.schedule)
|
|
||||||
.next()
|
|
||||||
.toDate();
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...cron,
|
...cron,
|
||||||
@@ -770,16 +731,10 @@ const Crontab = () => {
|
|||||||
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -863,32 +818,160 @@ const Crontab = () => {
|
|||||||
|
|
||||||
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]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
getCronViews();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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);
|
||||||
|
const firstEnableView = data.filter((x) => !x.isDisabled);
|
||||||
|
setEnabledCronViews(firstEnableView);
|
||||||
setPageConf({
|
setPageConf({
|
||||||
page: 1,
|
page: 1,
|
||||||
size: parseInt(localStorage.getItem('pageSize') || '20'),
|
size: parseInt(localStorage.getItem('pageSize') || '20'),
|
||||||
sorter: {},
|
sorter: {},
|
||||||
filters: {},
|
filters: {},
|
||||||
});
|
});
|
||||||
getCronViews();
|
setViewConf({
|
||||||
}, []);
|
...firstEnableView[0],
|
||||||
|
});
|
||||||
useEffect(() => {
|
|
||||||
if (tableRef.current) {
|
|
||||||
setTableScrollHeight(getTableScroll());
|
|
||||||
}
|
}
|
||||||
}, []);
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const panelContent = (
|
const tabClick = (key: string) => {
|
||||||
<>
|
const view = enabledCronViews.find((x) => x.id == key);
|
||||||
|
setSelectedRowIds([]);
|
||||||
|
setPageConf({ ...pageConf, page: 1 });
|
||||||
|
setViewConf(view ? view : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [vt] = useVT(
|
||||||
|
() => ({ scroll: { y: tableScrollHeight } }),
|
||||||
|
[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
|
||||||
@@ -943,7 +1026,6 @@ const Crontab = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Table
|
<Table
|
||||||
ref={tableRef}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: pageConf.page,
|
current: pageConf.page,
|
||||||
@@ -965,149 +1047,9 @@ const Crontab = () => {
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
rowClassName={getRowClassName}
|
rowClassName={getRowClassName}
|
||||||
onChange={onPageChange}
|
onChange={onPageChange}
|
||||||
|
// components={isPhone ? undefined : vt}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
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(({ 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);
|
|
||||||
};
|
|
||||||
|
|
||||||
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}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: 'all',
|
|
||||||
label: '全部任务',
|
|
||||||
children: panelContent,
|
|
||||||
},
|
|
||||||
...[...enabledCronViews].slice(0, 2).map((x) => ({
|
|
||||||
key: x.id,
|
|
||||||
label: x.name,
|
|
||||||
children: panelContent,
|
|
||||||
})),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<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';
|
||||||
@@ -34,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) {
|
||||||
@@ -45,14 +45,20 @@ const CronLogModal = ({
|
|||||||
.then(({ code, data }) => {
|
.then(({ code, data }) => {
|
||||||
if (
|
if (
|
||||||
code === 200 &&
|
code === 200 &&
|
||||||
localStorage.getItem('logCron') === String(cron.id)
|
localStorage.getItem('logCron') === String(cron.id) &&
|
||||||
|
data !== value
|
||||||
) {
|
) {
|
||||||
const log = data as string;
|
const log = data as string;
|
||||||
setValue(log || '暂无日志');
|
setValue(log || '暂无日志');
|
||||||
setExecuting(
|
const hasNext = Boolean(
|
||||||
log && !logEnded(log) && !log.includes('重启面板'),
|
log &&
|
||||||
|
!logEnded(log) &&
|
||||||
|
!log.includes('重启面板') &&
|
||||||
|
!log.includes('任务未运行'),
|
||||||
);
|
);
|
||||||
if (log && !logEnded(log) && !log.includes('重启面板')) {
|
setExecuting(hasNext);
|
||||||
|
autoScroll();
|
||||||
|
if (hasNext) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getCronLog();
|
getCronLog();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -89,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 (
|
||||||
<>
|
<>
|
||||||
@@ -138,6 +166,7 @@ const CronLogModal = ({
|
|||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
<div onScroll={handleScroll} className="log-container">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
) : (
|
) : (
|
||||||
@@ -155,6 +184,8 @@ const CronLogModal = ({
|
|||||||
{value}
|
{value}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
|
<div id="log-flag"></div>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -74,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
|
||||||
|
|||||||
@@ -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,12 @@ 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 filtersValue = Form.useWatch('filters', form);
|
||||||
|
|
||||||
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](
|
||||||
@@ -87,12 +105,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}
|
||||||
@@ -114,7 +127,7 @@ const ViewCreateModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const typeElement = (
|
const typeElement = (
|
||||||
<Select>
|
<Select style={{ width: 80 }}>
|
||||||
{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,15 +136,17 @@ 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
|
||||||
@@ -164,21 +179,67 @@ 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 3px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setFilterRelation(
|
||||||
|
filterRelation === 'and' ? 'or' : 'and',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
{fields.map(({ key, name, ...restField }, index) => (
|
<span>{ViewFilterRelation[filterRelation]}</span>
|
||||||
|
<IconFont type="ql-icon-d-caret" />
|
||||||
|
</>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={index === 0 ? '筛选条件' : ''}
|
label={name === 0 ? '筛选条件' : ''}
|
||||||
key={key}
|
key={key}
|
||||||
style={{ marginBottom: 0 }}
|
style={{ marginBottom: 0 }}
|
||||||
required
|
required
|
||||||
|
className="filter-item"
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
className="view-create-modal-filters"
|
||||||
|
align="baseline"
|
||||||
>
|
>
|
||||||
<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, { width: 120 })}
|
{propertyElement(PROPERTIES, { width: 90 })}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
@@ -192,15 +253,14 @@ const ViewCreateModal = ({
|
|||||||
name={[name, 'value']}
|
name={[name, 'value']}
|
||||||
rules={[{ required: true, message: '请输入内容' }]}
|
rules={[{ required: true, message: '请输入内容' }]}
|
||||||
>
|
>
|
||||||
{['In', 'Nin'].includes(
|
{EOperation[filtersValue?.[name]['operation']] ===
|
||||||
form.getFieldValue(['filters', index, 'operation']),
|
'select' ? (
|
||||||
) ? (
|
statusElement(filtersValue?.[name]['property'])
|
||||||
statusElement
|
|
||||||
) : (
|
) : (
|
||||||
<Input placeholder="请输入内容" />
|
<Input placeholder="请输入内容" />
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{index !== 0 && (
|
{name !== 0 && (
|
||||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -208,23 +268,63 @@ 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">
|
||||||
{(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 3px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
{fields.map(({ key, name, ...restField }, index) => (
|
<span>{ViewFilterRelation[filterRelation]}</span>
|
||||||
|
</>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={index === 0 ? '排序方式' : ''}
|
label={name === 0 ? '排序方式' : ''}
|
||||||
key={key}
|
key={key}
|
||||||
style={{ marginBottom: 0 }}
|
style={{ marginBottom: 0 }}
|
||||||
|
className="filter-item"
|
||||||
>
|
>
|
||||||
<Space className="view-create-modal-sorts" align="baseline">
|
<Space className="view-create-modal-sorts" align="baseline">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -232,7 +332,7 @@ const ViewCreateModal = ({
|
|||||||
name={[name, 'property']}
|
name={[name, 'property']}
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
>
|
>
|
||||||
{propertyElement(PROPERTIES, { width: 240 })}
|
{propertyElement(PROPERTIES)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
@@ -251,7 +351,8 @@ const ViewCreateModal = ({
|
|||||||
新增排序方式
|
新增排序方式
|
||||||
</a>
|
</a>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -75,22 +75,31 @@ const ViewManageModal = ({
|
|||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
cronViewChange: (data?: any) => void;
|
cronViewChange: (data?: any) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const islastEnableView = (record) => {
|
||||||
|
return list.filter((x) => !x.isDisabled).length <= 1 && !record.isDisabled;
|
||||||
|
};
|
||||||
|
|
||||||
const columns: any = [
|
const columns: any = [
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
align: 'center' as const,
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
key: 'type',
|
||||||
|
render: (v) => (v === 1 ? '系统' : '个人'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '显示',
|
title: '显示',
|
||||||
key: 'isDisabled',
|
key: 'isDisabled',
|
||||||
dataIndex: 'isDisabled',
|
dataIndex: 'isDisabled',
|
||||||
align: 'center' as const,
|
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
|
disabled={islastEnableView(record)}
|
||||||
checked={!record.isDisabled}
|
checked={!record.isDisabled}
|
||||||
onChange={(checked) => onShowChange(checked, record, index)}
|
onChange={(checked) => onShowChange(checked, record, index)}
|
||||||
/>
|
/>
|
||||||
@@ -100,18 +109,21 @@ const ViewManageModal = ({
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 140,
|
width: 100,
|
||||||
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 />
|
||||||
</a>
|
</a>
|
||||||
|
{!islastEnableView(record) && (
|
||||||
<a onClick={() => deleteView(record, index)}>
|
<a onClick={() => deleteView(record, index)}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</a>
|
</a>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -155,7 +167,6 @@ 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],
|
||||||
|
|||||||
+100
-73
@@ -19,6 +19,8 @@ import {
|
|||||||
DeleteFilled,
|
DeleteFilled,
|
||||||
BugOutlined,
|
BugOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
} 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';
|
||||||
@@ -27,10 +29,10 @@ 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 { useOutletContext } from '@umijs/max';
|
||||||
import { SharedContext } from '@/layouts';
|
import { SharedContext } from '@/layouts';
|
||||||
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -42,6 +44,7 @@ enum Status {
|
|||||||
'删除中',
|
'删除中',
|
||||||
'已删除',
|
'已删除',
|
||||||
'删除失败',
|
'删除失败',
|
||||||
|
'队列中',
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StatusColor {
|
enum StatusColor {
|
||||||
@@ -50,13 +53,43 @@ enum StatusColor {
|
|||||||
'error',
|
'error',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StatusMap: Record<number, { icon: React.ReactNode; color: string }> = {
|
||||||
|
0: {
|
||||||
|
icon: <SyncOutlined spin />,
|
||||||
|
color: 'processing',
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
icon: <CheckCircleOutlined />,
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
icon: <CloseCircleOutlined />,
|
||||||
|
color: 'error',
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
icon: <SyncOutlined spin />,
|
||||||
|
color: 'processing',
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
icon: <CheckCircleOutlined />,
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
icon: <CloseCircleOutlined />,
|
||||||
|
color: 'error',
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
icon: <ClockCircleOutlined />,
|
||||||
|
color: 'default',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const Dependence = () => {
|
const Dependence = () => {
|
||||||
const { headerStyle, isPhone, socketMessage } =
|
const { headerStyle, isPhone, socketMessage } =
|
||||||
useOutletContext<SharedContext>();
|
useOutletContext<SharedContext>();
|
||||||
const columns: any = [
|
const columns: any = [
|
||||||
{
|
{
|
||||||
title: '序号',
|
title: '序号',
|
||||||
align: 'center' as const,
|
|
||||||
width: 50,
|
width: 50,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
||||||
@@ -66,18 +99,17 @@ const Dependence = () => {
|
|||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
align: 'center' as const,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
align: 'center' as const,
|
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
return (
|
return (
|
||||||
<Space size="middle" style={{ cursor: 'text' }}>
|
<Space size="middle" style={{ cursor: 'text' }}>
|
||||||
<Tag
|
<Tag
|
||||||
color={StatusColor[record.status % 3]}
|
color={StatusMap[record.status].color}
|
||||||
|
icon={StatusMap[record.status].icon}
|
||||||
style={{ marginRight: 0 }}
|
style={{ marginRight: 0 }}
|
||||||
>
|
>
|
||||||
{Status[record.status]}
|
{Status[record.status]}
|
||||||
@@ -90,13 +122,11 @@ const Dependence = () => {
|
|||||||
title: '备注',
|
title: '备注',
|
||||||
dataIndex: 'remark',
|
dataIndex: 'remark',
|
||||||
key: 'remark',
|
key: 'remark',
|
||||||
align: 'center' as const,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
dataIndex: 'timestamp',
|
dataIndex: 'timestamp',
|
||||||
align: 'center' as const,
|
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
const language = navigator.language || navigator.languages[0];
|
||||||
const time = record.createdAt || record.timestamp;
|
const time = record.createdAt || record.timestamp;
|
||||||
@@ -119,7 +149,6 @@ const Dependence = () => {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
align: 'center' as const,
|
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
const isPc = !isPhone;
|
const isPc = !isPhone;
|
||||||
return (
|
return (
|
||||||
@@ -164,11 +193,11 @@ const Dependence = () => {
|
|||||||
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 tableRef = useRef<any>();
|
||||||
|
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
|
||||||
|
|
||||||
const getDependencies = () => {
|
const getDependencies = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -217,7 +246,7 @@ const Dependence = () => {
|
|||||||
})
|
})
|
||||||
.then(({ code, data }) => {
|
.then(({ code, data }) => {
|
||||||
if (code === 200 && force) {
|
if (code === 200 && force) {
|
||||||
const i = value.findIndex((x) => x.id === data.data[0].id);
|
const i = value.findIndex((x) => x.id === data[0].id);
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
result.splice(i, 1);
|
result.splice(i, 1);
|
||||||
@@ -283,16 +312,10 @@ const Dependence = () => {
|
|||||||
|
|
||||||
const onSelectChange = (selectedIds: any[]) => {
|
const onSelectChange = (selectedIds: any[]) => {
|
||||||
setSelectedRowIds(selectedIds);
|
setSelectedRowIds(selectedIds);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
|
|
||||||
setTableScrollHeight(getTableScroll({ extraHeight: 59 }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowSelection = {
|
const rowSelection = {
|
||||||
selectedRowIds,
|
selectedRowKeys: selectedRowIds,
|
||||||
onChange: onSelectChange,
|
onChange: onSelectChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -368,12 +391,6 @@ const Dependence = () => {
|
|||||||
getDependencies();
|
getDependencies();
|
||||||
}, [searchText, type]);
|
}, [searchText, type]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (tableRef.current) {
|
|
||||||
setTableScrollHeight(getTableScroll({ extraHeight: 59 }));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (logDependence) {
|
if (logDependence) {
|
||||||
localStorage.setItem('logDependence', logDependence.id);
|
localStorage.setItem('logDependence', logDependence.id);
|
||||||
@@ -384,6 +401,23 @@ const Dependence = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socketMessage) return;
|
if (!socketMessage) return;
|
||||||
const { type, message, references } = socketMessage;
|
const { type, message, references } = socketMessage;
|
||||||
|
if (
|
||||||
|
type === 'installDependence' &&
|
||||||
|
message.includes('开始时间') &&
|
||||||
|
references.length > 0
|
||||||
|
) {
|
||||||
|
const result = [...value];
|
||||||
|
for (let i = 0; i < references.length; i++) {
|
||||||
|
const index = value.findIndex((x) => x.id === references[i]);
|
||||||
|
if (index !== -1) {
|
||||||
|
result.splice(index, 1, {
|
||||||
|
...value[index],
|
||||||
|
status: message.includes('安装') ? Status.安装中 : Status.删除中,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setValue(result);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
type === 'installDependence' &&
|
type === 'installDependence' &&
|
||||||
message.includes('结束时间') &&
|
message.includes('结束时间') &&
|
||||||
@@ -422,53 +456,6 @@ const Dependence = () => {
|
|||||||
}
|
}
|
||||||
}, [socketMessage]);
|
}, [socketMessage]);
|
||||||
|
|
||||||
const panelContent = () => (
|
|
||||||
<>
|
|
||||||
{selectedRowIds.length > 0 && (
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
style={{ marginBottom: 5, marginLeft: 8 }}
|
|
||||||
onClick={() => handlereInstallDependencies()}
|
|
||||||
>
|
|
||||||
批量安装
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
style={{ marginBottom: 5, marginLeft: 8 }}
|
|
||||||
onClick={() => delDependencies(false)}
|
|
||||||
>
|
|
||||||
批量删除
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
style={{ marginBottom: 5, marginLeft: 8 }}
|
|
||||||
onClick={() => delDependencies(true)}
|
|
||||||
>
|
|
||||||
批量强制删除
|
|
||||||
</Button>
|
|
||||||
<span style={{ marginLeft: 8 }}>
|
|
||||||
已选择
|
|
||||||
<a>{selectedRowIds?.length}</a>项
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<DndProvider backend={HTML5Backend}>
|
|
||||||
<Table
|
|
||||||
ref={tableRef}
|
|
||||||
columns={columns}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
pagination={false}
|
|
||||||
dataSource={value}
|
|
||||||
rowKey="id"
|
|
||||||
size="middle"
|
|
||||||
scroll={{ x: 768, y: tableScrollHeight }}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
</DndProvider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTabChange = (activeKey: string) => {
|
const onTabChange = (activeKey: string) => {
|
||||||
setSelectedRowIds([]);
|
setSelectedRowIds([]);
|
||||||
setType(activeKey);
|
setType(activeKey);
|
||||||
@@ -503,20 +490,60 @@ const Dependence = () => {
|
|||||||
{
|
{
|
||||||
key: 'nodejs',
|
key: 'nodejs',
|
||||||
label: 'NodeJs',
|
label: 'NodeJs',
|
||||||
children: panelContent(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'python3',
|
key: 'python3',
|
||||||
label: 'Python3',
|
label: 'Python3',
|
||||||
children: panelContent(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'linux',
|
key: 'linux',
|
||||||
label: 'Linux',
|
label: 'Linux',
|
||||||
children: panelContent(),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<div ref={tableRef}>
|
||||||
|
{selectedRowIds.length > 0 && (
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginBottom: 5, marginLeft: 8 }}
|
||||||
|
onClick={() => handlereInstallDependencies()}
|
||||||
|
>
|
||||||
|
批量安装
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginBottom: 5, marginLeft: 8 }}
|
||||||
|
onClick={() => delDependencies(false)}
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginBottom: 5, marginLeft: 8 }}
|
||||||
|
onClick={() => delDependencies(true)}
|
||||||
|
>
|
||||||
|
批量强制删除
|
||||||
|
</Button>
|
||||||
|
<span style={{ marginLeft: 8 }}>
|
||||||
|
已选择
|
||||||
|
<a>{selectedRowIds?.length}</a>项
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={value}
|
||||||
|
rowKey="id"
|
||||||
|
size="middle"
|
||||||
|
scroll={{ x: 768, y: tableScrollHeight }}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</DndProvider>
|
||||||
|
</div>
|
||||||
<DependenceModal
|
<DependenceModal
|
||||||
visible={isModalVisible}
|
visible={isModalVisible}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user