mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
046239404f | ||
|
|
b90243f55c | ||
|
|
43c0cd8132 | ||
|
|
533e12a796 | ||
|
|
bd004a0489 | ||
|
|
41befcc0a8 | ||
|
|
9fb9b3d121 | ||
|
|
5055045d22 | ||
|
|
9f7beb934d | ||
|
|
26b06c17c5 | ||
|
|
b8a9b26ca3 | ||
|
|
b6376ed2e8 | ||
|
|
99f6073c8e | ||
|
|
5e73f0390f | ||
|
|
c35cfba8b0 | ||
|
|
aac109621a | ||
|
|
a340964c82 | ||
|
|
00818b694a | ||
|
|
ec5b885476 | ||
|
|
9d55cb108c | ||
|
|
4c19054b30 | ||
|
|
2a41f64d1b | ||
|
|
d3023d31e3 | ||
|
|
eddc03e295 | ||
|
|
77a8e00b17 | ||
|
|
99281a061e | ||
|
|
22eedebf14 | ||
|
|
d7e0531935 | ||
|
|
16734326cf | ||
|
|
956cfe18be | ||
|
|
a864a56917 | ||
|
|
ab3fc9b5f1 | ||
|
|
8d899f1a53 | ||
|
|
a1eef2c644 | ||
|
|
aab096843c | ||
|
|
acc7443004 | ||
|
|
042d7d3b8e | ||
|
|
0511a4af0d | ||
|
|
748a099087 | ||
|
|
742d0b2135 | ||
|
|
2081364325 | ||
|
|
39dc31916e | ||
|
|
5395cebdb4 | ||
|
|
fe934fa4af | ||
|
|
461fa8e131 | ||
|
|
4618a19c04 | ||
|
|
e2bd15683e | ||
|
|
648b9c4520 | ||
|
|
b4e4e84bbd | ||
|
|
aa5d6f3cb6 | ||
|
|
e9416c23df | ||
|
|
ef4999be55 | ||
|
|
e7be4999b0 | ||
|
|
5907553670 | ||
|
|
0b9066525a | ||
|
|
ac904cae61 | ||
|
|
4d5fa320ea | ||
|
|
003defedcf | ||
|
|
40e8041401 | ||
|
|
a8174e89be | ||
|
|
cce83f7a4c | ||
|
|
4f7649f157 | ||
|
|
b002cbef3a | ||
|
|
1d93fe0de0 | ||
|
|
2d936f1341 | ||
|
|
b40673e9ac | ||
|
|
637874e426 | ||
|
|
de71d1a258 | ||
|
|
e174e190ee | ||
|
|
89164a3bd2 | ||
|
|
a9b9c4d7ad | ||
|
|
f947866c32 | ||
|
|
a2e33d1ed4 | ||
|
|
b1a242c8ee | ||
|
|
0bf28e9ab3 | ||
|
|
624c9374e5 | ||
|
|
a9da8fffc1 | ||
|
|
b4e5db9da9 | ||
|
|
2e27d4057e | ||
|
|
e7d023a7e0 | ||
|
|
39bfd39559 | ||
|
|
6832cb4e22 | ||
|
|
590dbba19b | ||
|
|
3987ef8d97 | ||
|
|
8b0dedaf8c | ||
|
|
04edd97967 | ||
|
|
6131fe0110 | ||
|
|
665c344ae4 | ||
|
|
373b8c97d7 | ||
|
|
56eb0c5408 | ||
|
|
fe55929959 | ||
|
|
01e2bd007d | ||
|
|
4e091b0c3e | ||
|
|
db94cd3799 | ||
|
|
a15192b9b2 | ||
|
|
efd4f1d5ab | ||
|
|
bd166ee794 | ||
|
|
93e94ea94c | ||
|
|
88b87de391 | ||
|
|
8affff96f3 | ||
|
|
936b565fb1 | ||
|
|
b69ff2895e | ||
|
|
f3791cbb62 | ||
|
|
b0f3b51736 | ||
|
|
3aa112e373 | ||
|
|
683482e067 | ||
|
|
5a2caeb66b | ||
|
|
ee6e5bd8b4 | ||
|
|
f970322f0a | ||
|
|
1718120623 | ||
|
|
702c3160ec | ||
|
|
db227e56bf | ||
|
|
84828865f7 | ||
|
|
87455cebc0 | ||
|
|
9f4435b237 | ||
|
|
e5bc4b3b04 | ||
|
|
11e1b50000 | ||
|
|
bec226ae13 | ||
|
|
c9ecdc6c97 | ||
|
|
4cf64e7af0 | ||
|
|
4ce01c5f0e | ||
|
|
2eff99abe1 | ||
|
|
5bf57185d9 | ||
|
|
c59dac19ae | ||
|
|
f1ef8e72ae | ||
|
|
5b1accffb7 | ||
|
|
f64ff83414 | ||
|
|
26c914d1db | ||
|
|
8f75a54110 | ||
|
|
874b6e3a56 | ||
|
|
f305d45225 | ||
|
|
a7f97bd729 | ||
|
|
0bbb7c7b89 | ||
|
|
f7465759c7 | ||
|
|
998e82d323 | ||
|
|
f8dfee8945 | ||
|
|
86e3d8736b | ||
|
|
85d32d00e1 | ||
|
|
6a52e2f804 | ||
|
|
b4cb72c5d0 | ||
|
|
79fd4ec58f | ||
|
|
49ed5c6632 | ||
|
|
ffba7ee4a1 | ||
|
|
91d080472f | ||
|
|
826f214f0f | ||
|
|
ca150dc6a6 | ||
|
|
ae5db60211 | ||
|
|
490bdc15f6 | ||
|
|
1377fe31aa | ||
|
|
3d1cf6bf2e | ||
|
|
6ac755b478 | ||
|
|
e70fa83074 | ||
|
|
41521f1d08 | ||
|
|
8b7bef097c | ||
|
|
618756d4a9 | ||
|
|
70d0c71414 | ||
|
|
65eb9c806d | ||
|
|
50718ba714 | ||
|
|
7c3bb90d12 | ||
|
|
f66390eabd | ||
|
|
481646501c | ||
|
|
5baa8e4391 | ||
|
|
3d11210aeb | ||
|
|
e1863407f2 | ||
|
|
0c28b63ab7 | ||
|
|
8b8bd279c6 | ||
|
|
01b404765e | ||
|
|
c231a871d7 | ||
|
|
598c6b4e57 | ||
|
|
2295fabcc5 | ||
|
|
be94d796ac | ||
|
|
c84908d7fa | ||
|
|
d8ae039b92 | ||
|
|
bce1431d03 | ||
|
|
9db0095e29 | ||
|
|
77dc7817fb | ||
|
|
d0dd97631e | ||
|
|
d11d6d0c18 | ||
|
|
0af687f781 | ||
|
|
8db997abe8 | ||
|
|
aab6bbeb15 | ||
|
|
df1addc1ff | ||
|
|
1f8f35476a | ||
|
|
5354fc76db | ||
|
|
b29a9e012c | ||
|
|
6e5d89c197 | ||
|
|
d6cfb18f06 | ||
|
|
9a3e38051d | ||
|
|
b705ad6ee8 | ||
|
|
db564d1add | ||
|
|
4f422c0658 | ||
|
|
f267720af4 | ||
|
|
4a0c66bcc0 | ||
|
|
ef7283a9fd | ||
|
|
4498449eea | ||
|
|
ebb9676a51 | ||
|
|
6fb39ce835 | ||
|
|
59c26d90d3 | ||
|
|
1d2df860e8 | ||
|
|
ce22cad5b2 | ||
|
|
dc44ce6b1a | ||
|
|
b27ee23cc3 | ||
|
|
083c8869aa | ||
|
|
6a971a0d6e | ||
|
|
a25bfb6912 | ||
|
|
f19dd21155 | ||
|
|
79b342d1d2 | ||
|
|
20b0d75b0d | ||
|
|
c89724906e | ||
|
|
c3a548ece3 | ||
|
|
b431e39f8f | ||
|
|
710746809b | ||
|
|
8a225e9beb | ||
|
|
a5ecb204b2 | ||
|
|
f1f009da3b | ||
|
|
7291098f98 | ||
|
|
666545ff03 | ||
|
|
81e2bbb14c | ||
|
|
970dff144c | ||
|
|
819b15b15e | ||
|
|
3fa57c69ac | ||
|
|
18689c8119 | ||
|
|
61e7049baa | ||
|
|
39c40a328d | ||
|
|
955d815d14 | ||
|
|
3a3b945de8 | ||
|
|
73c53dbc8d | ||
|
|
f76828d4ec | ||
|
|
7b2956eb0a | ||
|
|
b2167ae5e4 | ||
|
|
98dfa4c425 | ||
|
|
5bf08b76de | ||
|
|
5c5b4f804e | ||
|
|
6455245d11 | ||
|
|
2ff3d9c601 | ||
|
|
d342fbf29f | ||
|
|
f865018cd6 | ||
|
|
3b9a4f0834 | ||
|
|
67bc305950 | ||
|
|
97f128d59f | ||
|
|
e25885c4d9 | ||
|
|
c285026339 | ||
|
|
6c34045a48 | ||
|
|
e1e6261b4f | ||
|
|
697bcb5922 | ||
|
|
e05b0a2491 | ||
|
|
e1655455d8 | ||
|
|
9921dab064 | ||
|
|
403ab7f0f0 | ||
|
|
9bc7ca4094 | ||
|
|
e924aa76b0 | ||
|
|
0c46606e14 | ||
|
|
dbfa4049f5 | ||
|
|
622fe2a8f8 | ||
|
|
7bce5c4f6a | ||
|
|
1f7f2c8971 | ||
|
|
437a849c2a | ||
|
|
8f7261dd50 | ||
|
|
76fa82c3a7 | ||
|
|
46c9774efa | ||
|
|
1bc64a23e3 | ||
|
|
cc9a17afa3 | ||
|
|
fcbcfc3a03 | ||
|
|
0144f3b131 | ||
|
|
518436d5ca | ||
|
|
1961a6b913 | ||
|
|
5edebdd5f3 | ||
|
|
48edf2c212 | ||
|
|
9b16dca9da | ||
|
|
9479c2498c | ||
|
|
4a18bcabe2 | ||
|
|
34f690c53d |
@@ -6,12 +6,9 @@ on:
|
|||||||
- 'master'
|
- 'master'
|
||||||
- 'develop'
|
- 'develop'
|
||||||
tags:
|
tags:
|
||||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
- 'v*'
|
||||||
schedule:
|
schedule:
|
||||||
# 参考 https://jasonet.co/posts/scheduled-actions/
|
- cron: '00 20 * * *'
|
||||||
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
|
|
||||||
# minute hour day month dayOfWeek
|
|
||||||
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -47,7 +44,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: '8.3.1'
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
@@ -89,17 +86,18 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
needs: build-static
|
needs: build-static
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: '8.3.1'
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -110,13 +108,13 @@ jobs:
|
|||||||
timezone: Asia/Shanghai
|
timezone: Asia/Shanghai
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -124,38 +122,41 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ github.repository }}
|
${{ github.repository }}
|
||||||
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}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
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 }}
|
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: .
|
context: .
|
||||||
file: ./docker/Dockerfile
|
file: ./docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
+2
-1
@@ -28,4 +28,5 @@
|
|||||||
/db
|
/db
|
||||||
/manual_log
|
/manual_log
|
||||||
/scripts
|
/scripts
|
||||||
/bak
|
/bak
|
||||||
|
/.tmp
|
||||||
@@ -6,3 +6,27 @@ package.json
|
|||||||
.umi
|
.umi
|
||||||
.umi-production
|
.umi-production
|
||||||
.umi-test
|
.umi-test
|
||||||
|
.history
|
||||||
|
.tmp
|
||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-error.log
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
static
|
||||||
|
data
|
||||||
|
DS_Store
|
||||||
|
src/.umi
|
||||||
|
src/.umi-production
|
||||||
|
src/.umi-test
|
||||||
|
.env.local
|
||||||
|
.env
|
||||||
|
history
|
||||||
|
version.ts
|
||||||
|
config
|
||||||
|
log
|
||||||
|
db
|
||||||
|
manual_log
|
||||||
|
scripts
|
||||||
|
bak
|
||||||
|
.tmp
|
||||||
@@ -1,25 +1,31 @@
|
|||||||
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,
|
||||||
|
jsMinifier: 'terser',
|
||||||
antd: {},
|
antd: {},
|
||||||
|
locale: {
|
||||||
|
antd: true,
|
||||||
|
title: true,
|
||||||
|
baseNavigator: true,
|
||||||
|
},
|
||||||
outputPath: 'static/dist',
|
outputPath: 'static/dist',
|
||||||
fastRefresh: true,
|
fastRefresh: true,
|
||||||
favicons: ['./images/favicon.svg'],
|
favicons: [`https://qn.whyour.cn/favicon.svg`],
|
||||||
mfsu: {
|
|
||||||
strategy: 'eager',
|
|
||||||
},
|
|
||||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
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) => {
|
||||||
@@ -37,6 +43,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',
|
||||||
],
|
],
|
||||||
|
|||||||
+135
-141
@@ -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
|
||||||
|
|
||||||
@@ -36,100 +35,49 @@ Timed task management panel with python3, javaScript, shell, typescript support
|
|||||||
- Support dark mode
|
- Support dark mode
|
||||||
- Support cell phone operation
|
- Support cell phone operation
|
||||||
|
|
||||||
## Deployment
|
## Version
|
||||||
|
|
||||||
### Local Deployment
|
### docker
|
||||||
|
|
||||||
|
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# To be refined, see the development steps first (not supported on windows yet)
|
docker pull whyour/qinglong:latest
|
||||||
|
docker pull whyour/qinglong:debian
|
||||||
```
|
```
|
||||||
|
|
||||||
### Podman Deployment
|
### npm
|
||||||
|
|
||||||
1. podman installation
|
The npm version supports `debian/ubuntu/centos/alpine` systems and requires `node/python3` to be installed.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
https://podman.io/getting-started/installation
|
npm i @whyour/qinglong
|
||||||
```
|
```
|
||||||
|
|
||||||
2. start the container
|
## Built-in commands
|
||||||
|
|
||||||
|
1. task
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
podman run -dit \
|
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
|
||||||
--network bridge \
|
task <file_path>
|
||||||
-v $PWD/ql/data:/ql/data \
|
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
|
||||||
-p 5700:5700 \
|
# and the foreground will output the day, while recorded in the log file
|
||||||
--name qinglong \
|
task <file_path> now
|
||||||
--hostname qinglong \
|
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
|
||||||
docker.io/whyour/qinglong:latest
|
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
|
||||||
|
task <file_path> conc <env_name> <account_number>(Optional)
|
||||||
|
# Specify the account to execute and run immediately regardless of whether a random delay is set
|
||||||
|
task <file_path> desi <env_name> <account_number>
|
||||||
|
# Set task timeout
|
||||||
|
task -m <max_time> <file_path>
|
||||||
|
# Print task log in real time, no need to carry this parameter when creating timed tasks
|
||||||
|
task -l <file_path>
|
||||||
|
# Use -- to split, -- followed by a parameter that is passed to the script, as in the following example, the script receives the parameter -u whyour -p password
|
||||||
|
task <file_path> -- -u whyour -p password
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker Deployment
|
1. ql
|
||||||
|
|
||||||
1. docker installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -sSL get.docker.com | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. configure domestic mirror sources
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /etc/docker
|
|
||||||
tee /etc/docker/daemon.json <<-'EOF'
|
|
||||||
{
|
|
||||||
"registry-mirrors": [
|
|
||||||
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
|
|
||||||
"https://ypzju6vq.mirror.aliyuncs.com",
|
|
||||||
"https://registry.docker-cn.com",
|
|
||||||
"http://hub-mirror.c.163.com",
|
|
||||||
"https://docker.mirrors.ustc.edu.cn"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart docker
|
|
||||||
```
|
|
||||||
|
|
||||||
3. start the container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -dit \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
-p 5700:5700 \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
--restart unless-stopped \
|
|
||||||
whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker-compose Deployment
|
|
||||||
|
|
||||||
1. docker-compose installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
2. start the container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir qinglong
|
|
||||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
|
||||||
|
|
||||||
# start
|
|
||||||
docker-compose up -d
|
|
||||||
# stop
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
3. access
|
|
||||||
|
|
||||||
Open your browser and visit http://{ip}:5700
|
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
1. built-in commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Update and restart Green Dragon
|
# Update and restart Green Dragon
|
||||||
@@ -150,36 +98,100 @@ ql check
|
|||||||
ql resetlet
|
ql resetlet
|
||||||
# Disable two-step login
|
# Disable two-step login
|
||||||
ql resettfa
|
ql resettfa
|
||||||
|
|
||||||
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
|
|
||||||
task <file_path>
|
|
||||||
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
|
|
||||||
# and the foreground will output the day, while recorded in the log file
|
|
||||||
task <file_path> now
|
|
||||||
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
|
|
||||||
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
|
|
||||||
task <file_path> conc <env_name> <account_number>(Optional)
|
|
||||||
# Specify the account to execute and run immediately regardless of whether a random delay is set
|
|
||||||
task <file_path> desi <env_name> <account_number>
|
|
||||||
# Set task timeout
|
|
||||||
task -m <max_time> <file_path>
|
|
||||||
# Print task log in real time, no need to carry this parameter when creating timed tasks
|
|
||||||
task -l <file_path>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. parameter description
|
1. parameter description
|
||||||
|
|
||||||
* file_url: Script address
|
- file_url: Script address
|
||||||
* repo_url: Repository address
|
- repo_url: Repository address
|
||||||
* whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
|
- whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
|
||||||
* blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
|
- blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
|
||||||
* dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
|
- dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
|
||||||
* branch: Pull the branch of the repository
|
- branch: Pull the branch of the repository
|
||||||
* days: Number of days of logs to be kept
|
- days: Number of days of logs to be kept
|
||||||
* 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
|
- max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# curl -sSL get.docker.com | sh
|
||||||
|
docker run -dit \
|
||||||
|
-v $PWD/ql/data:/ql/data \
|
||||||
|
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||||
|
-p 5700:5700 \
|
||||||
|
# Deployment paths are not required, e.g. /test.
|
||||||
|
-e QlBaseUrl="/" \
|
||||||
|
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
||||||
|
-e QlPort="5700" \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
--restart unless-stopped \
|
||||||
|
whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker-compose (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
|
mkdir qinglong
|
||||||
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||||
|
|
||||||
|
# start
|
||||||
|
docker-compose up -d
|
||||||
|
# stop
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Podman (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# https://podman.io/getting-started/installation
|
||||||
|
podman run -dit \
|
||||||
|
--network bridge \
|
||||||
|
-v $PWD/ql/data:/ql/data \
|
||||||
|
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||||
|
-p 5700:5700 \
|
||||||
|
# Deployment paths are not required, e.g. /test.
|
||||||
|
-e QlBaseUrl="/" \
|
||||||
|
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
||||||
|
-e QlPort="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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
@@ -191,28 +203,10 @@ task -l <file_path>
|
|||||||
- [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".
|
||||||
|
|
||||||
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
|
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
|
||||||
|
|
||||||
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
||||||
|
|||||||
@@ -1,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)
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
@@ -36,122 +37,30 @@
|
|||||||
- 支持暗黑模式
|
- 支持暗黑模式
|
||||||
- 支持手机端操作
|
- 支持手机端操作
|
||||||
|
|
||||||
## 部署
|
## 版本
|
||||||
|
|
||||||
### 本机部署
|
### docker
|
||||||
|
|
||||||
|
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 待完善,可先参考开发步骤 (windows暂时不支持)
|
docker pull whyour/qinglong:latest
|
||||||
|
docker pull whyour/qinglong:debian
|
||||||
```
|
```
|
||||||
|
|
||||||
### podman 部署
|
### npm
|
||||||
|
|
||||||
1. podman 安装
|
npm 版本支持 `debian/ubuntu/centos/alpine` 系统,需要自行安装 `node/python3`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
https://podman.io/getting-started/installation
|
npm i @whyour/qinglong
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 启动容器
|
## 内置命令
|
||||||
|
|
||||||
|
1. task
|
||||||
|
|
||||||
```bash
|
```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
|
|
||||||
docker run -dit \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
-p 5700:5700 \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
--restart unless-stopped \
|
|
||||||
whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### docker-compose 部署
|
|
||||||
|
|
||||||
1. docker-compose 安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 启动容器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir qinglong
|
|
||||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
|
||||||
|
|
||||||
# 启动
|
|
||||||
docker-compose up -d
|
|
||||||
# 停止
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 访问
|
|
||||||
|
|
||||||
打开你的浏览器,访问 http://{ip}:5700
|
|
||||||
|
|
||||||
## 使用
|
|
||||||
|
|
||||||
1. 内置命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 更新并重启青龙
|
|
||||||
ql update
|
|
||||||
# 运行自定义脚本extra.sh
|
|
||||||
ql extra
|
|
||||||
# 添加单个脚本文件
|
|
||||||
ql raw <file_url>
|
|
||||||
# 添加单个仓库的指定脚本
|
|
||||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
|
||||||
# 删除旧日志
|
|
||||||
ql rmlog <days>
|
|
||||||
# 启动tg-bot
|
|
||||||
ql bot
|
|
||||||
# 检测青龙环境并修复
|
|
||||||
ql check
|
|
||||||
# 重置登录错误次数
|
|
||||||
ql resetlet
|
|
||||||
# 禁用两步登录
|
|
||||||
ql resettfa
|
|
||||||
|
|
||||||
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||||
task <file_path>
|
task <file_path>
|
||||||
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||||
@@ -164,22 +73,126 @@ task <file_path> desi <env_name> <account_number>
|
|||||||
task -m <max_time> <file_path>
|
task -m <max_time> <file_path>
|
||||||
# 实时打印任务日志,创建定时任务时,不用携带此参数
|
# 实时打印任务日志,创建定时任务时,不用携带此参数
|
||||||
task -l <file_path>
|
task -l <file_path>
|
||||||
|
# 使用 -- 分割,-- 后面的参数会传给脚本,下面的例子,脚本就可接收到参数 -u whyour -p password
|
||||||
|
task <file_path> -- -u whyour -p password
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 参数说明
|
1. ql
|
||||||
|
|
||||||
* file_url: 脚本地址
|
```bash
|
||||||
* repo_url: 仓库地址
|
# 更新并重启青龙
|
||||||
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
|
ql update
|
||||||
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
|
# 运行自定义脚本extra.sh
|
||||||
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
|
ql extra
|
||||||
* extensions: 拉取仓库的文件后缀,多个竖线分割
|
# 添加单个脚本文件
|
||||||
* branch: 拉取仓库的分支
|
ql raw <file_url>
|
||||||
* days: 需要保留的日志的天数
|
# 添加单个仓库的指定脚本
|
||||||
* file_path: 任务执行时的文件路径
|
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
||||||
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
# 删除旧日志
|
||||||
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
ql rmlog <days>
|
||||||
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
# 启动tg-bot
|
||||||
|
ql bot
|
||||||
|
# 检测青龙环境并修复
|
||||||
|
ql check
|
||||||
|
# 重置登录错误次数
|
||||||
|
ql resetlet
|
||||||
|
# 禁用两步登录
|
||||||
|
ql resettfa
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 参数说明
|
||||||
|
|
||||||
|
- file_url: 脚本地址
|
||||||
|
- repo_url: 仓库地址
|
||||||
|
- whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
|
||||||
|
- blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
|
||||||
|
- dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
|
||||||
|
- extensions: 拉取仓库的文件后缀,多个竖线分割
|
||||||
|
- branch: 拉取仓库的分支
|
||||||
|
- days: 需要保留的日志的天数
|
||||||
|
- file_path: 任务执行时的文件路径
|
||||||
|
- env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||||
|
- account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||||
|
- max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
### docker (推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# curl -sSL get.docker.com | sh
|
||||||
|
docker run -dit \
|
||||||
|
-v $PWD/ql/data:/ql/data \
|
||||||
|
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||||
|
-p 5700:5700 \
|
||||||
|
# 部署路径非必须,比如 /test
|
||||||
|
-e QlBaseUrl="/" \
|
||||||
|
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||||
|
-e QlPort="5700" \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
--restart unless-stopped \
|
||||||
|
whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### docker-compose (推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
|
mkdir qinglong
|
||||||
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
docker-compose up -d
|
||||||
|
# 停止
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### podman (推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# https://podman.io/getting-started/installation
|
||||||
|
podman run -dit \
|
||||||
|
--network bridge \
|
||||||
|
-v $PWD/ql/data:/ql/data \
|
||||||
|
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||||
|
-p 5700:5700 \
|
||||||
|
# 部署路径非必须,比如 /test
|
||||||
|
-e QlBaseUrl="/" \
|
||||||
|
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||||
|
-e QlPort="5700" \
|
||||||
|
--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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
@@ -191,24 +204,6 @@ task -l <file_path>
|
|||||||
- [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) {
|
||||||
|
|||||||
+25
-3
@@ -152,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({
|
||||||
@@ -160,6 +175,10 @@ 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),
|
||||||
|
extra_schedules: Joi.array().optional().allow(null),
|
||||||
|
task_before: Joi.string().optional().allow('').allow(null),
|
||||||
|
task_after: Joi.string().optional().allow('').allow(null),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -316,6 +335,10 @@ 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),
|
||||||
|
extra_schedules: Joi.array().optional().allow(null),
|
||||||
|
task_before: Joi.string().optional().allow('').allow(null),
|
||||||
|
task_after: Joi.string().optional().allow('').allow(null),
|
||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -435,13 +458,12 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.status({
|
const data = await cronService.status({
|
||||||
...req.body,
|
...req.body,
|
||||||
status: parseInt(req.body.status),
|
status: req.body.status ? parseInt(req.body.status) : undefined,
|
||||||
pid: parseInt(req.body.pid) || '',
|
pid: req.body.pid ? parseInt(req.body.pid) : undefined,
|
||||||
});
|
});
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+2
-1
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
|
|||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { safeJSONParse } from '../config/util';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
@@ -200,7 +201,7 @@ export default (app: Router) => {
|
|||||||
try {
|
try {
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
|
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
|
||||||
const parseContent = JSON.parse(fileContent);
|
const parseContent = safeJSONParse(fileContent);
|
||||||
const data = Array.isArray(parseContent)
|
const data = Array.isArray(parseContent)
|
||||||
? parseContent
|
? parseContent
|
||||||
: [parseContent];
|
: [parseContent];
|
||||||
|
|||||||
+2
-3
@@ -53,7 +53,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) => {
|
||||||
@@ -65,7 +65,7 @@ export default (app: Router) => {
|
|||||||
};
|
};
|
||||||
const filePath = join(config.logPath, path, filename);
|
const filePath = join(config.logPath, path, filename);
|
||||||
if (type === 'directory') {
|
if (type === 'directory') {
|
||||||
emptyDir(filePath);
|
await emptyDir(filePath);
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
@@ -75,5 +75,4 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-6
@@ -34,7 +34,7 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let result = [];
|
let result = [];
|
||||||
const blacklist = ['node_modules', '.git'];
|
const blacklist = ['node_modules', '.git', '.pnpm'];
|
||||||
if (req.query.path) {
|
if (req.query.path) {
|
||||||
const targetPath = path.join(
|
const targetPath = path.join(
|
||||||
config.scriptPath,
|
config.scriptPath,
|
||||||
@@ -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);
|
||||||
@@ -183,7 +183,7 @@ export default (app: Router) => {
|
|||||||
};
|
};
|
||||||
const filePath = join(config.scriptPath, path, filename);
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
if (type === 'directory') {
|
if (type === 'directory') {
|
||||||
emptyDir(filePath);
|
await emptyDir(filePath);
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
@@ -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', //告诉浏览器这是一个二进制文件
|
||||||
@@ -260,14 +260,17 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
let { filename, path, pid } = 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 logPath = join(config.logPath, path, `${name}.swap`);
|
||||||
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.stopScript(filePath, pid);
|
const result = await scriptService.stopScript(filePath, pid);
|
||||||
|
setTimeout(() => {
|
||||||
|
emptyDir(logPath);
|
||||||
|
}, 3000);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export default (app: Router) => {
|
|||||||
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),
|
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) => {
|
||||||
@@ -179,6 +181,8 @@ export default (app: Router) => {
|
|||||||
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),
|
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(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -206,12 +210,16 @@ export default (app: Router) => {
|
|||||||
'/',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
query: Joi.object({
|
||||||
|
force: Joi.boolean().optional(),
|
||||||
|
t: Joi.number()
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const data = await subscriptionService.remove(req.body);
|
const data = await subscriptionService.remove(req.body, req.query);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
|
|||||||
+148
-30
@@ -7,10 +7,25 @@ import SystemService from '../services/system';
|
|||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
import { EnvModel } from '../data/env';
|
import { EnvModel } from '../data/env';
|
||||||
import { parseVersion, promiseExec } from '../config/util';
|
import {
|
||||||
|
getUniqPath,
|
||||||
|
handleLogPath,
|
||||||
|
parseVersion,
|
||||||
|
promiseExec,
|
||||||
|
} from '../config/util';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import multer from 'multer';
|
||||||
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.tmpPath);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, 'data.tgz');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/system', route);
|
app.use('/system', route);
|
||||||
@@ -20,30 +35,14 @@ export default (app: Router) => {
|
|||||||
try {
|
try {
|
||||||
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 { version, changeLog, changeLogLink, publishTime } =
|
||||||
const { version, changeLog, changeLogLink } = await parseVersion(
|
await parseVersion(config.versionFile);
|
||||||
config.versionFile,
|
|
||||||
);
|
|
||||||
const lastCommitTime = (
|
|
||||||
await promiseExec(
|
|
||||||
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
|
|
||||||
)
|
|
||||||
).replace('\n', '');
|
|
||||||
const lastCommitId = (
|
|
||||||
await promiseExec(`cd ${config.rootPath} && git rev-parse --short HEAD`)
|
|
||||||
).replace('\n', '');
|
|
||||||
const branch = (
|
|
||||||
await promiseExec(
|
|
||||||
`cd ${config.rootPath} && git symbolic-ref --short HEAD`,
|
|
||||||
)
|
|
||||||
).replace('\n', '');
|
|
||||||
|
|
||||||
let isInitialized = true;
|
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;
|
||||||
}
|
}
|
||||||
@@ -52,9 +51,8 @@ export default (app: Router) => {
|
|||||||
data: {
|
data: {
|
||||||
isInitialized,
|
isInitialized,
|
||||||
version,
|
version,
|
||||||
lastCommitTime: dayjs(lastCommitTime).unix(),
|
publishTime: dayjs(publishTime).unix(),
|
||||||
lastCommitId,
|
branch: process.env.QL_BRANCH || 'master',
|
||||||
branch,
|
|
||||||
changeLog,
|
changeLog,
|
||||||
changeLogLink,
|
changeLogLink,
|
||||||
},
|
},
|
||||||
@@ -66,12 +64,12 @@ export default (app: Router) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/log/remove',
|
'/config',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
const data = await systemService.getLogRemoveFrequency();
|
const data = await systemService.getSystemConfig();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -80,19 +78,18 @@ export default (app: Router) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/log/remove',
|
'/config',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
frequency: Joi.number().required(),
|
logRemoveFrequency: Joi.number().optional().allow(null),
|
||||||
|
cronConcurrency: Joi.number().optional().allow(null),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
const result = await systemService.updateLogRemoveFrequency(
|
const result = await systemService.updateSystemConfig(req.body);
|
||||||
req.body.frequency,
|
|
||||||
);
|
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -128,6 +125,25 @@ export default (app: Router) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/reload',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
type: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.reloadSystem(req.body.type);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/notify',
|
'/notify',
|
||||||
celebrate({
|
celebrate({
|
||||||
@@ -147,4 +163,106 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/command-run',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
command: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const command = req.body.command
|
||||||
|
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
|
||||||
|
let id = await promiseExec(idStr);
|
||||||
|
const uniqPath = await getUniqPath(command, id);
|
||||||
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
|
const logPath = `${uniqPath}/${logTime}.log`;
|
||||||
|
res.setHeader('Content-type', 'application/octet-stream');
|
||||||
|
await systemService.run(
|
||||||
|
{ ...req.body, logPath },
|
||||||
|
{
|
||||||
|
onStart: async (cp, startTime) => {
|
||||||
|
res.setHeader('QL-Task-Pid', `${cp.pid}`);
|
||||||
|
},
|
||||||
|
onEnd: async (cp, endTime, diff) => {
|
||||||
|
res.end();
|
||||||
|
},
|
||||||
|
onError: async (message: string) => {
|
||||||
|
res.write(`\n${message}`);
|
||||||
|
const absolutePath = await handleLogPath(logPath);
|
||||||
|
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||||
|
},
|
||||||
|
onLog: async (message: string) => {
|
||||||
|
res.write(`\n${message}`);
|
||||||
|
const absolutePath = await handleLogPath(logPath);
|
||||||
|
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/command-stop',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
command: Joi.string().optional(),
|
||||||
|
pid: Joi.number().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.stop(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/data/export',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
await systemService.exportData(res);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/data/import',
|
||||||
|
upload.single('data'),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.importData();
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/log',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
await systemService.getSystemLog(res);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
|
|||||||
import multer from 'multer';
|
import 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(),
|
||||||
|
|||||||
+3
-1
@@ -2,7 +2,6 @@ import 'reflect-metadata'; // We need this in order to use @Decorators
|
|||||||
import config from './config';
|
import config from './config';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -18,9 +17,12 @@ async function startServer() {
|
|||||||
const server = app
|
const server = app
|
||||||
.listen(config.port, () => {
|
.listen(config.port, () => {
|
||||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
Logger.debug(`✌️ 后端服务启动成功!`);
|
||||||
|
console.debug(`✌️ 后端服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const LOG_END_SYMBOL = '\n ';
|
export const LOG_END_SYMBOL = ' ';
|
||||||
|
|
||||||
export const TASK_COMMAND = 'task';
|
export const TASK_COMMAND = 'task';
|
||||||
export const QL_COMMAND = 'ql';
|
export const QL_COMMAND = 'ql';
|
||||||
|
|||||||
+18
-1
@@ -1,6 +1,6 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createRandomString } from './util';
|
import { createRandomString } from './share';
|
||||||
|
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ 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') });
|
||||||
|
|
||||||
const dataPath = path.join(rootPath, 'data/');
|
const dataPath = path.join(rootPath, 'data/');
|
||||||
|
const shellPath = path.join(rootPath, 'shell/');
|
||||||
|
const tmpPath = path.join(rootPath, '.tmp/');
|
||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
const configPath = path.join(dataPath, 'config/');
|
const configPath = path.join(dataPath, 'config/');
|
||||||
const scriptPath = path.join(dataPath, 'scripts/');
|
const scriptPath = path.join(dataPath, 'scripts/');
|
||||||
@@ -27,6 +29,8 @@ 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 systemLogPath = path.join(dataPath, 'syslog/');
|
||||||
|
|
||||||
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');
|
||||||
@@ -41,6 +45,8 @@ 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, 'version.yaml');
|
const versionFile = path.join(rootPath, 'version.yaml');
|
||||||
|
const dataTgzFile = path.join(tmpPath, 'data.tgz');
|
||||||
|
const shareShellFile = path.join(shellPath, 'share.sh');
|
||||||
|
|
||||||
if (envFound.error) {
|
if (envFound.error) {
|
||||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||||
@@ -58,6 +64,10 @@ export default {
|
|||||||
prefix: '/api',
|
prefix: '/api',
|
||||||
},
|
},
|
||||||
rootPath,
|
rootPath,
|
||||||
|
tmpPath,
|
||||||
|
dataPath,
|
||||||
|
dataTgzFile,
|
||||||
|
shareShellFile,
|
||||||
configString,
|
configString,
|
||||||
loginFaild,
|
loginFaild,
|
||||||
authError,
|
authError,
|
||||||
@@ -91,8 +101,15 @@ 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,
|
||||||
|
systemLogPath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import pick from 'lodash/pick';
|
||||||
|
|
||||||
|
let pickedEnv: Record<string, string>;
|
||||||
|
|
||||||
|
function getPickedEnv() {
|
||||||
|
if (pickedEnv) return pickedEnv;
|
||||||
|
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
|
||||||
|
if (picked.QlBaseUrl) {
|
||||||
|
if (!picked.QlBaseUrl.startsWith('/')) {
|
||||||
|
picked.QlBaseUrl = `/${picked.QlBaseUrl}`
|
||||||
|
}
|
||||||
|
if (!picked.QlBaseUrl.endsWith('/')) {
|
||||||
|
picked.QlBaseUrl = `${picked.QlBaseUrl}/`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pickedEnv = picked as Record<string, string>;
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serveEnv(_req: Request, res: Response) {
|
||||||
|
res.type('.js');
|
||||||
|
res.send(
|
||||||
|
Object.entries(getPickedEnv())
|
||||||
|
.map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)
|
||||||
|
.join('\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
export function createRandomString(min: number, max: number): string {
|
||||||
|
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
|
const english = [
|
||||||
|
'a',
|
||||||
|
'b',
|
||||||
|
'c',
|
||||||
|
'd',
|
||||||
|
'e',
|
||||||
|
'f',
|
||||||
|
'g',
|
||||||
|
'h',
|
||||||
|
'i',
|
||||||
|
'j',
|
||||||
|
'k',
|
||||||
|
'l',
|
||||||
|
'm',
|
||||||
|
'n',
|
||||||
|
'o',
|
||||||
|
'p',
|
||||||
|
'q',
|
||||||
|
'r',
|
||||||
|
's',
|
||||||
|
't',
|
||||||
|
'u',
|
||||||
|
'v',
|
||||||
|
'w',
|
||||||
|
'x',
|
||||||
|
'y',
|
||||||
|
'z',
|
||||||
|
];
|
||||||
|
const ENGLISH = [
|
||||||
|
'A',
|
||||||
|
'B',
|
||||||
|
'C',
|
||||||
|
'D',
|
||||||
|
'E',
|
||||||
|
'F',
|
||||||
|
'G',
|
||||||
|
'H',
|
||||||
|
'I',
|
||||||
|
'J',
|
||||||
|
'K',
|
||||||
|
'L',
|
||||||
|
'M',
|
||||||
|
'N',
|
||||||
|
'O',
|
||||||
|
'P',
|
||||||
|
'Q',
|
||||||
|
'R',
|
||||||
|
'S',
|
||||||
|
'T',
|
||||||
|
'U',
|
||||||
|
'V',
|
||||||
|
'W',
|
||||||
|
'X',
|
||||||
|
'Y',
|
||||||
|
'Z',
|
||||||
|
];
|
||||||
|
const special = ['-', '_'];
|
||||||
|
const config = num.concat(english).concat(ENGLISH).concat(special);
|
||||||
|
|
||||||
|
const arr = [];
|
||||||
|
arr.push(getOne(num));
|
||||||
|
arr.push(getOne(english));
|
||||||
|
arr.push(getOne(ENGLISH));
|
||||||
|
arr.push(getOne(special));
|
||||||
|
|
||||||
|
const len = min + Math.floor(Math.random() * (max - min + 1));
|
||||||
|
|
||||||
|
for (let i = 4; i < len; i++) {
|
||||||
|
arr.push(config[Math.floor(Math.random() * config.length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newArr = [];
|
||||||
|
for (let j = 0; j < len; j++) {
|
||||||
|
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOne(arr: any[]) {
|
||||||
|
return arr[Math.floor(Math.random() * arr.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArr.join('');
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Subscription } from '../data/subscription';
|
||||||
|
import isNil from 'lodash/isNil';
|
||||||
|
|
||||||
|
export function formatUrl(doc: Subscription) {
|
||||||
|
let url = doc.url;
|
||||||
|
let host = '';
|
||||||
|
if (doc.type === 'private-repo') {
|
||||||
|
if (doc.pull_type === 'ssh-key') {
|
||||||
|
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
|
||||||
|
url = doc.url!.replace(host, doc.alias);
|
||||||
|
} else {
|
||||||
|
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
|
||||||
|
const { username, password } = doc.pull_option as any;
|
||||||
|
url = doc.url!.replace(host, `${username}:${password}@${host}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { url, host };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCommand(doc: Subscription, url?: string) {
|
||||||
|
let command = `SUB_ID=${doc.id} ql `;
|
||||||
|
let _url = url || formatUrl(doc).url;
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
whitelist,
|
||||||
|
blacklist,
|
||||||
|
dependences,
|
||||||
|
branch,
|
||||||
|
extensions,
|
||||||
|
proxy,
|
||||||
|
autoAddCron,
|
||||||
|
autoDelCron,
|
||||||
|
} = doc;
|
||||||
|
if (type === 'file') {
|
||||||
|
command += `raw "${_url}"`;
|
||||||
|
} else {
|
||||||
|
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
||||||
|
dependences || ''
|
||||||
|
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${
|
||||||
|
isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||||
|
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
+116
-111
@@ -7,6 +7,11 @@ import FormData from 'form-data';
|
|||||||
import psTreeFun from 'pstree.remy';
|
import psTreeFun from 'pstree.remy';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { load } from 'js-yaml';
|
import { load } from 'js-yaml';
|
||||||
|
import config from './index';
|
||||||
|
import { TASK_COMMAND } from './const';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
export * from './share';
|
||||||
|
|
||||||
export function getFileContentByName(fileName: string) {
|
export function getFileContentByName(fileName: string) {
|
||||||
if (fs.existsSync(fileName)) {
|
if (fs.existsSync(fileName)) {
|
||||||
@@ -34,91 +39,6 @@ export function getLastModifyFilePath(dir: string) {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRandomString(min: number, max: number): string {
|
|
||||||
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
||||||
const english = [
|
|
||||||
'a',
|
|
||||||
'b',
|
|
||||||
'c',
|
|
||||||
'd',
|
|
||||||
'e',
|
|
||||||
'f',
|
|
||||||
'g',
|
|
||||||
'h',
|
|
||||||
'i',
|
|
||||||
'j',
|
|
||||||
'k',
|
|
||||||
'l',
|
|
||||||
'm',
|
|
||||||
'n',
|
|
||||||
'o',
|
|
||||||
'p',
|
|
||||||
'q',
|
|
||||||
'r',
|
|
||||||
's',
|
|
||||||
't',
|
|
||||||
'u',
|
|
||||||
'v',
|
|
||||||
'w',
|
|
||||||
'x',
|
|
||||||
'y',
|
|
||||||
'z',
|
|
||||||
];
|
|
||||||
const ENGLISH = [
|
|
||||||
'A',
|
|
||||||
'B',
|
|
||||||
'C',
|
|
||||||
'D',
|
|
||||||
'E',
|
|
||||||
'F',
|
|
||||||
'G',
|
|
||||||
'H',
|
|
||||||
'I',
|
|
||||||
'J',
|
|
||||||
'K',
|
|
||||||
'L',
|
|
||||||
'M',
|
|
||||||
'N',
|
|
||||||
'O',
|
|
||||||
'P',
|
|
||||||
'Q',
|
|
||||||
'R',
|
|
||||||
'S',
|
|
||||||
'T',
|
|
||||||
'U',
|
|
||||||
'V',
|
|
||||||
'W',
|
|
||||||
'X',
|
|
||||||
'Y',
|
|
||||||
'Z',
|
|
||||||
];
|
|
||||||
const special = ['-', '_'];
|
|
||||||
const config = num.concat(english).concat(ENGLISH).concat(special);
|
|
||||||
|
|
||||||
const arr = [];
|
|
||||||
arr.push(getOne(num));
|
|
||||||
arr.push(getOne(english));
|
|
||||||
arr.push(getOne(ENGLISH));
|
|
||||||
arr.push(getOne(special));
|
|
||||||
|
|
||||||
const len = min + Math.floor(Math.random() * (max - min + 1));
|
|
||||||
|
|
||||||
for (let i = 4; i < len; i++) {
|
|
||||||
arr.push(config[Math.floor(Math.random() * config.length)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newArr = [];
|
|
||||||
for (let j = 0; j < len; j++) {
|
|
||||||
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOne(arr: any[]) {
|
|
||||||
return arr[Math.floor(Math.random() * arr.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return newArr.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getToken(req: any) {
|
export function getToken(req: any) {
|
||||||
const { authorization = '' } = req.headers;
|
const { authorization = '' } = req.headers;
|
||||||
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
||||||
@@ -164,6 +84,11 @@ export async function getNetIp(req: any) {
|
|||||||
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
||||||
ip = '';
|
ip = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ip) {
|
||||||
|
return { address: `获取失败`, ip };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baiduApi = got
|
const baiduApi = got
|
||||||
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
||||||
@@ -245,6 +170,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,
|
||||||
@@ -293,22 +230,28 @@ interface IFile {
|
|||||||
type: 'directory' | 'file';
|
type: 'directory' | 'file';
|
||||||
parent: string;
|
parent: string;
|
||||||
mtime: number;
|
mtime: number;
|
||||||
|
size?: number;
|
||||||
children?: IFile[];
|
children?: IFile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dirSort(a: IFile, b: IFile) {
|
export function dirSort(a: IFile, b: IFile): number {
|
||||||
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1;
|
if (a.type !== b.type) {
|
||||||
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1;
|
return FileType[a.type] < FileType[b.type] ? -1 : 1
|
||||||
|
} else if (a.mtime !== b.mtime) {
|
||||||
|
return a.mtime > b.mtime ? -1 : 1
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDirs(
|
export function readDirs(
|
||||||
dir: string,
|
dir: string,
|
||||||
baseDir: string = '',
|
baseDir: string = '',
|
||||||
blacklist: string[] = [],
|
blacklist: string[] = [],
|
||||||
) {
|
): IFile[] {
|
||||||
const relativePath = path.relative(baseDir, dir);
|
const relativePath = path.relative(baseDir, dir);
|
||||||
const files = fs.readdirSync(dir);
|
const files = fs.readdirSync(dir);
|
||||||
const result: any = files
|
const result: IFile[] = files
|
||||||
.filter((x) => !blacklist.includes(x))
|
.filter((x) => !blacklist.includes(x))
|
||||||
.map((file: string) => {
|
.map((file: string) => {
|
||||||
const subPath = path.join(dir, file);
|
const subPath = path.join(dir, file);
|
||||||
@@ -330,6 +273,7 @@ export function readDirs(
|
|||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
key,
|
key,
|
||||||
parent: relativePath,
|
parent: relativePath,
|
||||||
|
size: stats.size,
|
||||||
mtime: stats.mtime.getTime(),
|
mtime: stats.mtime.getTime(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -359,30 +303,40 @@ export function readDir(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emptyDir(path: string) {
|
export async function emptyDir(path: string) {
|
||||||
|
const pathExist = await fileExist(path);
|
||||||
|
if (!pathExist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const files = fs.readdirSync(path);
|
const files = fs.readdirSync(path);
|
||||||
files.forEach((file) => {
|
for (const file of files) {
|
||||||
const filePath = `${path}/${file}`;
|
const filePath = `${path}/${file}`;
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
emptyDir(filePath);
|
await emptyDir(filePath);
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
fs.rmdirSync(path);
|
fs.rmdirSync(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function promiseExec(command: string): Promise<string> {
|
export async function promiseExec(command: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
exec(
|
const { stderr, stdout } = await promisify(exec)(command, { maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' });
|
||||||
command,
|
return stdout || stderr;
|
||||||
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
|
} catch (error) {
|
||||||
(err, stdout, stderr) => {
|
return JSON.stringify(error);
|
||||||
resolve(stdout || stderr || JSON.stringify(err));
|
}
|
||||||
},
|
}
|
||||||
);
|
|
||||||
});
|
export async function promiseExecSuccess(command: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await promisify(exec)(command, { maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' });
|
||||||
|
return stdout || '';
|
||||||
|
} catch (error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseHeaders(headers: string) {
|
export function parseHeaders(headers: string) {
|
||||||
@@ -469,25 +423,29 @@ export function psTree(pid: number): Promise<number[]> {
|
|||||||
|
|
||||||
export async function killTask(pid: number) {
|
export async function killTask(pid: number) {
|
||||||
const pids = await psTree(pid);
|
const pids = await psTree(pid);
|
||||||
// SIGALRM 14 时钟信号
|
|
||||||
if (pids.length) {
|
if (pids.length) {
|
||||||
process.kill(pids[0], 14);
|
try {
|
||||||
|
[pid, ...pids].reverse().forEach((x) => {
|
||||||
|
process.kill(x, 15);
|
||||||
|
});
|
||||||
|
} catch (error) { }
|
||||||
} else {
|
} else {
|
||||||
process.kill(pid, 14);
|
process.kill(pid, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPid(name: string) {
|
export async function getPid(name: string) {
|
||||||
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
|
||||||
const execAsync = promisify(exec);
|
const pid = await promiseExec(taskCommand);
|
||||||
let pid = (await execAsync(taskCommand)).stdout;
|
return pid ? Number(pid) : undefined;
|
||||||
return Number(pid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IVersion {
|
interface IVersion {
|
||||||
version: string;
|
version: string;
|
||||||
changeLogLink: string;
|
changeLogLink: string;
|
||||||
changeLog: string;
|
changeLog: string;
|
||||||
|
publishTime: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseVersion(path: string): Promise<IVersion> {
|
export async function parseVersion(path: string): Promise<IVersion> {
|
||||||
@@ -497,3 +455,50 @@ export async function parseVersion(path: string): Promise<IVersion> {
|
|||||||
export async function parseContentVersion(content: string): Promise<IVersion> {
|
export async function parseContentVersion(content: string): Promise<IVersion> {
|
||||||
return load(content) as IVersion;
|
return load(content) as IVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUniqPath(command: string, id: string): Promise<string> {
|
||||||
|
if (/^\d+$/.test(id)) {
|
||||||
|
id = `_${id}`;
|
||||||
|
} else {
|
||||||
|
id = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = command.split(/ +/);
|
||||||
|
let str = items[0];
|
||||||
|
if (items[0] === TASK_COMMAND) {
|
||||||
|
str = items[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dotIndex = str.lastIndexOf('.');
|
||||||
|
|
||||||
|
if (dotIndex !== -1) {
|
||||||
|
str = str.slice(0, dotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slashIndex = str.lastIndexOf('/');
|
||||||
|
|
||||||
|
let tempStr = '';
|
||||||
|
if (slashIndex !== -1) {
|
||||||
|
tempStr = str.slice(0, slashIndex);
|
||||||
|
const _slashIndex = tempStr.lastIndexOf('/');
|
||||||
|
if (_slashIndex !== -1) {
|
||||||
|
tempStr = tempStr.slice(_slashIndex + 1);
|
||||||
|
}
|
||||||
|
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${str}${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeJSONParse(value?: string) {
|
||||||
|
if (!value) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[JSON.parse失败]', error)
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-2
@@ -1,10 +1,11 @@
|
|||||||
import { sequelize } from '.';
|
import { sequelize } from '.';
|
||||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||||
|
import { NotificationInfo } from './notify';
|
||||||
|
|
||||||
export class AuthInfo {
|
export class AuthInfo {
|
||||||
ip?: string;
|
ip?: string;
|
||||||
type: AuthDataType;
|
type: AuthDataType;
|
||||||
info?: any;
|
info?: AuthModelInfo;
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
||||||
constructor(options: AuthInfo) {
|
constructor(options: AuthInfo) {
|
||||||
@@ -25,9 +26,27 @@ export enum AuthDataType {
|
|||||||
'authToken' = 'authToken',
|
'authToken' = 'authToken',
|
||||||
'notification' = 'notification',
|
'notification' = 'notification',
|
||||||
'removeLogFrequency' = 'removeLogFrequency',
|
'removeLogFrequency' = 'removeLogFrequency',
|
||||||
|
'systemConfig' = 'systemConfig',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
|
export interface SystemConfigInfo {
|
||||||
|
logRemoveFrequency?: number;
|
||||||
|
cronConcurrency?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginLogInfo {
|
||||||
|
timestamp?: number;
|
||||||
|
address?: string;
|
||||||
|
ip?: string;
|
||||||
|
platform?: string;
|
||||||
|
status?: LoginStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthModelInfo = SystemConfigInfo &
|
||||||
|
Partial<NotificationInfo> &
|
||||||
|
LoginLogInfo;
|
||||||
|
|
||||||
|
export interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
|
||||||
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
|
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
|
||||||
ip: DataTypes.STRING,
|
ip: DataTypes.STRING,
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
|
|||||||
+18
-6
@@ -17,15 +17,19 @@ 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;
|
||||||
|
extra_schedules?: Array<{ schedule: string }>;
|
||||||
|
task_before?: string;
|
||||||
|
task_after?: string;
|
||||||
|
|
||||||
constructor(options: Crontab) {
|
constructor(options: Crontab) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.command = options.command;
|
this.command = options.command.trim();
|
||||||
this.schedule = options.schedule;
|
this.schedule = options.schedule;
|
||||||
this.saved = options.saved;
|
this.saved = options.saved;
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.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,17 +41,21 @@ 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;
|
||||||
|
this.extra_schedules = options.extra_schedules;
|
||||||
|
this.task_before = options.task_before;
|
||||||
|
this.task_after = options.task_after;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CrontabStatus {
|
export enum CrontabStatus {
|
||||||
'running',
|
'running' = 0,
|
||||||
'idle',
|
'queued' = 0.5,
|
||||||
|
'idle' = 1,
|
||||||
'disabled',
|
'disabled',
|
||||||
'queued',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { }
|
||||||
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||||
name: {
|
name: {
|
||||||
unique: 'compositeIndex',
|
unique: 'compositeIndex',
|
||||||
@@ -72,4 +80,8 @@ 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 },
|
||||||
|
extra_schedules: DataTypes.JSON,
|
||||||
|
task_before: DataTypes.STRING,
|
||||||
|
task_after: DataTypes.STRING,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class CrontabView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CronViewInstance
|
export interface CronViewInstance
|
||||||
extends Model<CrontabView, CrontabView>,
|
extends Model<CrontabView, CrontabView>,
|
||||||
CrontabView {}
|
CrontabView {}
|
||||||
export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
||||||
|
|||||||
+25
-9
@@ -4,18 +4,21 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|||||||
export class Dependence {
|
export class Dependence {
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
status?: DependenceStatus;
|
status: DependenceStatus;
|
||||||
type?: DependenceTypes;
|
type: DependenceTypes;
|
||||||
name?: number;
|
name: string;
|
||||||
log?: string[];
|
log?: string[];
|
||||||
remark?: string;
|
remark?: string;
|
||||||
|
|
||||||
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.trim();
|
||||||
this.log = options.log || [];
|
this.log = options.log || [];
|
||||||
this.remark = options.remark || '';
|
this.remark = options.remark || '';
|
||||||
}
|
}
|
||||||
@@ -28,6 +31,7 @@ export enum DependenceStatus {
|
|||||||
'removing',
|
'removing',
|
||||||
'removed',
|
'removed',
|
||||||
'removeFailed',
|
'removeFailed',
|
||||||
|
'queued',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DependenceTypes {
|
export enum DependenceTypes {
|
||||||
@@ -38,17 +42,29 @@ export enum DependenceTypes {
|
|||||||
|
|
||||||
export enum InstallDependenceCommandTypes {
|
export enum InstallDependenceCommandTypes {
|
||||||
'pnpm add -g',
|
'pnpm add -g',
|
||||||
'pip3 install',
|
'pip3 install --disable-pip-version-check --root-user-action=ignore',
|
||||||
'apk add',
|
'apk add --no-check-certificate',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GetDependenceCommandTypes {
|
||||||
|
'pnpm ls -g ',
|
||||||
|
'pip3 show --disable-pip-version-check',
|
||||||
|
'apk info -es',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum versionDependenceCommandTypes {
|
||||||
|
'@',
|
||||||
|
'==',
|
||||||
|
'=',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum unInstallDependenceCommandTypes {
|
export enum unInstallDependenceCommandTypes {
|
||||||
'pnpm remove -g',
|
'pnpm remove -g',
|
||||||
'pip3 uninstall -y',
|
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
||||||
'apk del',
|
'apk del',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DependenceInstance
|
export interface DependenceInstance
|
||||||
extends Model<Dependence, Dependence>,
|
extends Model<Dependence, Dependence>,
|
||||||
Dependence {}
|
Dependence {}
|
||||||
export const DependenceModel = sequelize.define<DependenceInstance>(
|
export const DependenceModel = sequelize.define<DependenceInstance>(
|
||||||
|
|||||||
+6
-3
@@ -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,10 +31,10 @@ 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 {}
|
export interface EnvInstance extends Model<Env, Env>, Env {}
|
||||||
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||||
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||||
timestamp: DataTypes.STRING,
|
timestamp: DataTypes.STRING,
|
||||||
|
|||||||
+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,
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ export enum NotificationMode {
|
|||||||
'iGot' = 'iGot',
|
'iGot' = 'iGot',
|
||||||
'pushPlus' = 'pushPlus',
|
'pushPlus' = 'pushPlus',
|
||||||
'email' = 'email',
|
'email' = 'email',
|
||||||
|
'pushMe' = 'pushMe',
|
||||||
'feishu' = 'feishu',
|
'feishu' = 'feishu',
|
||||||
'webhook' = 'webhook',
|
'webhook' = 'webhook',
|
||||||
|
'chronocat' = 'Chronocat',
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class NotificationBaseInfo {
|
abstract class NotificationBaseInfo {
|
||||||
@@ -54,6 +56,8 @@ export class BarkNotification extends NotificationBaseInfo {
|
|||||||
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
||||||
public barkSound = '';
|
public barkSound = '';
|
||||||
public barkGroup = 'qinglong';
|
public barkGroup = 'qinglong';
|
||||||
|
public barkLevel = 'active';
|
||||||
|
public barkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||||
@@ -72,10 +76,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 {
|
||||||
@@ -99,6 +105,16 @@ export class EmailNotification extends NotificationBaseInfo {
|
|||||||
public emailPass: string = '';
|
public emailPass: string = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PushMeNotification extends NotificationBaseInfo {
|
||||||
|
public pushMeKey: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChronocatNotification extends NotificationBaseInfo {
|
||||||
|
public chronocatURL: string = '';
|
||||||
|
public chronocatQQ: string = '';
|
||||||
|
public chronocatToekn: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
export class WebhookNotification extends NotificationBaseInfo {
|
export class WebhookNotification extends NotificationBaseInfo {
|
||||||
public webhookHeaders: string = '';
|
public webhookHeaders: string = '';
|
||||||
public webhookBody: string = '';
|
public webhookBody: string = '';
|
||||||
@@ -129,5 +145,8 @@ export interface NotificationInfo
|
|||||||
IGotNotification,
|
IGotNotification,
|
||||||
PushPlusNotification,
|
PushPlusNotification,
|
||||||
EmailNotification,
|
EmailNotification,
|
||||||
|
PushMeNotification,
|
||||||
WebhookNotification,
|
WebhookNotification,
|
||||||
|
ChronocatNotification,
|
||||||
LarkNotification {}
|
LarkNotification {}
|
||||||
|
|
||||||
|
|||||||
+1
-8
@@ -26,14 +26,7 @@ export interface AppToken {
|
|||||||
|
|
||||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
||||||
|
|
||||||
export enum CrontabStatus {
|
export interface AppInstance extends Model<App, App>, App {}
|
||||||
'running',
|
|
||||||
'idle',
|
|
||||||
'disabled',
|
|
||||||
'queued',
|
|
||||||
}
|
|
||||||
|
|
||||||
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' },
|
||||||
scopes: DataTypes.JSON,
|
scopes: DataTypes.JSON,
|
||||||
|
|||||||
+2
-1
@@ -16,4 +16,5 @@ export type SockMessageType =
|
|||||||
| 'uninstallDependence'
|
| 'uninstallDependence'
|
||||||
| 'updateSystemVersion'
|
| 'updateSystemVersion'
|
||||||
| 'manuallyRunScript'
|
| 'manuallyRunScript'
|
||||||
| 'runSubscriptionEnd';
|
| 'runSubscriptionEnd'
|
||||||
|
| 'reloadSystem';
|
||||||
|
|||||||
@@ -29,14 +29,16 @@ export class Subscription {
|
|||||||
sub_before?: string;
|
sub_before?: string;
|
||||||
sub_after?: string;
|
sub_after?: string;
|
||||||
proxy?: 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;
|
||||||
@@ -56,6 +58,8 @@ export class Subscription {
|
|||||||
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.proxy = options.proxy;
|
||||||
|
this.autoAddCron = options.autoAddCron ? 1 : 0;
|
||||||
|
this.autoDelCron = options.autoDelCron ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +70,7 @@ export enum SubscriptionStatus {
|
|||||||
'queued',
|
'queued',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubscriptionInstance
|
export interface SubscriptionInstance
|
||||||
extends Model<Subscription, Subscription>,
|
extends Model<Subscription, Subscription>,
|
||||||
Subscription {}
|
Subscription {}
|
||||||
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
||||||
@@ -105,5 +109,7 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
|||||||
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 },
|
proxy: { type: DataTypes.STRING, allowNull: true },
|
||||||
|
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
|
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Vendored
+2
@@ -3,3 +3,5 @@ declare namespace Express {
|
|||||||
platform: 'desktop' | 'mobile';
|
platform: 'desktop' | 'mobile';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'pstree.remy';
|
||||||
|
|||||||
+24
-14
@@ -7,18 +7,28 @@ import linkDeps from './deps';
|
|||||||
import initTask from './initTask';
|
import initTask from './initTask';
|
||||||
|
|
||||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||||
await depInjectorLoader();
|
try {
|
||||||
Logger.info('✌️ Dependency Injector loaded');
|
depInjectorLoader();
|
||||||
|
Logger.info('✌️ Dependency Injector loaded');
|
||||||
await expressLoader({ app: expressApp });
|
console.log('✌️ Dependency Injector loaded');
|
||||||
Logger.info('✌️ Express loaded');
|
|
||||||
|
expressLoader({ app: expressApp });
|
||||||
await initData();
|
Logger.info('✌️ Express loaded');
|
||||||
Logger.info('✌️ init data loaded');
|
console.log('✌️ Express loaded');
|
||||||
|
|
||||||
await linkDeps();
|
await initData();
|
||||||
Logger.info('✌️ link deps loaded');
|
Logger.info('✌️ init data loaded');
|
||||||
|
console.log('✌️ init data loaded');
|
||||||
initTask();
|
|
||||||
Logger.info('✌️ init task loaded');
|
await linkDeps();
|
||||||
|
Logger.info('✌️ link deps loaded');
|
||||||
|
console.log('✌️ link deps loaded');
|
||||||
|
|
||||||
|
initTask();
|
||||||
|
Logger.info('✌️ init task loaded');
|
||||||
|
console.log('✌️ init task loaded');
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed, ${error}`);
|
||||||
|
console.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed ${error}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+25
-2
@@ -36,6 +36,28 @@ export default async () => {
|
|||||||
try {
|
try {
|
||||||
await sequelize.query('alter table CrontabViews add column type NUMBER');
|
await sequelize.query('alter table CrontabViews add column type NUMBER');
|
||||||
} catch (error) {}
|
} 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) { }
|
||||||
|
try {
|
||||||
|
await sequelize.query('alter table Crontabs add column extra_schedules JSON');
|
||||||
|
} catch (error) { }
|
||||||
|
try {
|
||||||
|
await sequelize.query('alter table Crontabs add column task_before TEXT');
|
||||||
|
} catch (error) { }
|
||||||
|
try {
|
||||||
|
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
||||||
|
} 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');
|
||||||
@@ -109,9 +131,10 @@ export default async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('✌️ DB loaded');
|
||||||
Logger.info('✌️ DB loaded');
|
Logger.info('✌️ DB loaded');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.info('✌️ DB load failed');
|
console.error('✌️ DB load failed');
|
||||||
Logger.info(error);
|
Logger.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async function linkCommand() {
|
|||||||
if (fs.existsSync(target)) {
|
if (fs.existsSync(target)) {
|
||||||
fs.unlinkSync(target);
|
fs.unlinkSync(target);
|
||||||
}
|
}
|
||||||
fs.symlink(source, target, (err) => {});
|
fs.symlink(source, target, (err) => { });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-28
@@ -5,37 +5,40 @@ import routes from '../api';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import jwt, { UnauthorizedError } from 'express-jwt';
|
import jwt, { UnauthorizedError } from 'express-jwt';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { getPlatform, getToken } from '../config/util';
|
import { getPlatform, getToken, safeJSONParse } from '../config/util';
|
||||||
import Container from 'typedi';
|
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';
|
||||||
|
import Logger from './logger';
|
||||||
|
|
||||||
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://0.0.0.0:${config.publicPort}/api`,
|
||||||
return handler(req, res, {
|
changeOrigin: true,
|
||||||
public: 'static/dist',
|
pathRewrite: { '/api/public': '' },
|
||||||
rewrites: [{ source: '**', destination: '/index.html' }],
|
logProvider: () => Logger
|
||||||
});
|
}),
|
||||||
}
|
);
|
||||||
});
|
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
jwt({
|
jwt({
|
||||||
secret: config.secret as string,
|
secret: config.secret,
|
||||||
algorithms: ['HS384'],
|
algorithms: ['HS384'],
|
||||||
}).unless({
|
}).unless({
|
||||||
path: [...config.apiWhiteList, /^\/open\//],
|
path: [...config.apiWhiteList, /^\/open\//],
|
||||||
@@ -82,7 +85,7 @@ export default ({ app }: { app: Application }) => {
|
|||||||
|
|
||||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
if (data && headerToken) {
|
if (data && headerToken) {
|
||||||
const { token = '', tokens = {} } = JSON.parse(data);
|
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
@@ -108,8 +111,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 +172,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 },
|
||||||
|
|||||||
+12
-15
@@ -4,8 +4,7 @@ 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 { CrontabViewModel, CronViewType } from '../data/cronView';
|
||||||
@@ -31,27 +30,25 @@ export default async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 初始化更新所有任务状态为空闲
|
// 初始化更新所有任务状态为空闲
|
||||||
await CrontabModel.update(
|
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
|
||||||
{ status: CrontabStatus.idle },
|
|
||||||
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
|
|
||||||
);
|
|
||||||
|
|
||||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||||
DependenceModel.findAll({
|
DependenceModel.findAll({
|
||||||
where: {},
|
where: {},
|
||||||
order: [['type', 'DESC']],
|
order: [
|
||||||
|
['type', 'DESC'],
|
||||||
|
['createdAt', '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 任务
|
||||||
CrontabModel.findAll({
|
CrontabModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
isDisabled: { [Op.ne]: 1 },
|
isDisabled: { [Op.ne]: 1 },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import dotenv from 'dotenv';
|
|||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { fileExist } from '../config/util';
|
import { fileExist } from '../config/util';
|
||||||
|
|
||||||
const rootPath = process.env.QL_DIR as string;;
|
const rootPath = process.env.QL_DIR as string;
|
||||||
const dataPath = path.join(rootPath, 'data/');
|
const dataPath = path.join(rootPath, 'data/');
|
||||||
const configPath = path.join(dataPath, 'config/');
|
const configPath = path.join(dataPath, 'config/');
|
||||||
const scriptPath = path.join(dataPath, 'scripts/');
|
const scriptPath = path.join(dataPath, 'scripts/');
|
||||||
@@ -13,12 +13,22 @@ 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 sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
|
||||||
|
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
|
||||||
|
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
|
||||||
|
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
|
||||||
|
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
|
||||||
|
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
|
||||||
|
const TaskAfterFile = path.join(configPath, 'task_after.sh');
|
||||||
const homedir = os.homedir();
|
const homedir = os.homedir();
|
||||||
const sshPath = path.resolve(homedir, '.ssh');
|
const sshPath = path.resolve(homedir, '.ssh');
|
||||||
|
const sshdPath = path.join(dataPath, 'ssh.d');
|
||||||
|
const systemLogPath = path.join(dataPath, 'syslog');
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const authFileExist = await fileExist(authConfigFile);
|
const authFileExist = await fileExist(authConfigFile);
|
||||||
@@ -29,19 +39,18 @@ 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 systemLogDirExist = await fileExist(systemLogPath);
|
||||||
|
const tmpDirExist = await fileExist(tmpPath);
|
||||||
|
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
|
||||||
|
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
|
||||||
|
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
|
||||||
|
const TaskAfterFileExist = await fileExist(TaskAfterFile);
|
||||||
|
|
||||||
if (!configDirExist) {
|
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 +59,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,7 +75,41 @@ export default async () => {
|
|||||||
fs.mkdirSync(bakPath);
|
fs.mkdirSync(bakPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sshdDirExist) {
|
||||||
|
fs.mkdirSync(sshdPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!systemLogDirExist) {
|
||||||
|
fs.mkdirSync(systemLogPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化文件
|
||||||
|
if (!authFileExist) {
|
||||||
|
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confFileExist) {
|
||||||
|
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scriptNotifyJsFileExist) {
|
||||||
|
fs.writeFileSync(scriptNotifyJsFile, fs.readFileSync(sampleNotifyJsFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scriptNotifyPyFileExist) {
|
||||||
|
fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TaskBeforeFileExist) {
|
||||||
|
fs.writeFileSync(TaskBeforeFile, fs.readFileSync(sampleTaskShellFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TaskAfterFileExist) {
|
||||||
|
fs.writeFileSync(TaskAfterFile, fs.readFileSync(sampleTaskShellFile));
|
||||||
|
}
|
||||||
|
|
||||||
dotenv.config({ path: confFile });
|
dotenv.config({ path: confFile });
|
||||||
|
|
||||||
Logger.info('✌️ Init file down');
|
Logger.info('✌️ Init file down');
|
||||||
|
console.log('✌️ Init file down');
|
||||||
};
|
};
|
||||||
|
|||||||
+17
-17
@@ -1,9 +1,10 @@
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
import ScheduleService from '../services/schedule';
|
import ScheduleService, { ScheduleTaskType } 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}`;
|
||||||
}
|
}
|
||||||
@@ -20,32 +22,30 @@ export default async () => {
|
|||||||
id: NaN,
|
id: NaN,
|
||||||
name: '生成token',
|
name: '生成token',
|
||||||
command: tokenCommand,
|
command: tokenCommand,
|
||||||
};
|
} as ScheduleTaskType;
|
||||||
|
await scheduleService.cancelIntervalTask(cron);
|
||||||
scheduleService.createIntervalTask(cron, {
|
scheduleService.createIntervalTask(cron, {
|
||||||
days: 28,
|
days: 28,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 运行删除日志任务
|
// 运行删除日志任务
|
||||||
const data = await systemService.getLogRemoveFrequency();
|
const data = await systemService.getSystemConfig();
|
||||||
if (data && data.info && data.info.frequency) {
|
if (data && data.info && data.info.logRemoveFrequency) {
|
||||||
const cron = {
|
const rmlogCron = {
|
||||||
id: data.id,
|
id: data.id as number,
|
||||||
name: '删除日志',
|
name: '删除日志',
|
||||||
command: `ql rmlog ${data.info.frequency}`,
|
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||||
};
|
};
|
||||||
scheduleService.createIntervalTask(cron, {
|
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||||
days: data.info.frequency,
|
scheduleService.createIntervalTask(rmlogCron, {
|
||||||
|
days: data.info.logRemoveFrequency,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行所有订阅
|
// 运行所有订阅
|
||||||
|
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-21
@@ -1,32 +1,38 @@
|
|||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
|
import 'winston-daily-rotate-file';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
const transports = [];
|
const levelMap: Record<string, string> = {
|
||||||
if (process.env.NODE_ENV !== 'development') {
|
info: '🔵',
|
||||||
transports.push(new winston.transports.Console());
|
warn: '🟡',
|
||||||
} else {
|
error: '🔴',
|
||||||
transports.push(
|
debug: '🔶'
|
||||||
new winston.transports.Console({
|
|
||||||
format: winston.format.combine(
|
|
||||||
winston.format.cli(),
|
|
||||||
winston.format.splat(),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customFormat = winston.format.combine(
|
||||||
|
winston.format.splat(),
|
||||||
|
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
|
winston.format.align(),
|
||||||
|
winston.format.printf((i) => `[${levelMap[i.level]}${i.level}] [${[i.timestamp]}]: ${i.message}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
format: customFormat,
|
||||||
|
datePattern: "YYYY-MM-DD",
|
||||||
|
maxSize: "20m",
|
||||||
|
maxFiles: "7d",
|
||||||
|
};
|
||||||
|
|
||||||
const LoggerInstance = winston.createLogger({
|
const LoggerInstance = winston.createLogger({
|
||||||
level: config.logs.level,
|
level: config.logs.level,
|
||||||
levels: winston.config.npm.levels,
|
levels: winston.config.npm.levels,
|
||||||
format: winston.format.combine(
|
transports: [
|
||||||
winston.format.timestamp({
|
new winston.transports.DailyRotateFile({
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
filename: path.join(config.systemLogPath, '%DATE%.log'),
|
||||||
}),
|
...defaultOptions,
|
||||||
winston.format.errors({ stack: true }),
|
})
|
||||||
winston.format.splat(),
|
],
|
||||||
winston.format.json(),
|
|
||||||
),
|
|
||||||
transports,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LoggerInstance;
|
export default LoggerInstance;
|
||||||
|
|||||||
@@ -10,12 +10,18 @@ export default async ({ expressApp }: { expressApp: Application }) => {
|
|||||||
const { version } = await parseVersion(config.versionFile);
|
const { version } = await parseVersion(config.versionFile);
|
||||||
|
|
||||||
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: version,
|
release: version,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,4 +29,5 @@ export default async ({ expressApp }: { expressApp: Application }) => {
|
|||||||
expressApp.use(Sentry.Handlers.tracingHandler());
|
expressApp.use(Sentry.Handlers.tracingHandler());
|
||||||
|
|
||||||
Logger.info('✌️ Sentry loaded');
|
Logger.info('✌️ Sentry loaded');
|
||||||
|
console.log('✌️ Sentry loaded');
|
||||||
};
|
};
|
||||||
|
|||||||
+26
-4
@@ -5,12 +5,34 @@ import Sock from './sock';
|
|||||||
export default async ({ server }: { server: Server }) => {
|
export default async ({ server }: { server: Server }) => {
|
||||||
await Sock({ server });
|
await Sock({ server });
|
||||||
Logger.info('✌️ Sock loaded');
|
Logger.info('✌️ Sock loaded');
|
||||||
|
let exitTime = 0;
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', (singal) => {
|
||||||
Logger.info('✌️ Server need close');
|
Logger.warn(`Server need close, singal ${singal}`);
|
||||||
|
console.warn(`Server need close, singal ${singal}`);
|
||||||
|
exitTime++;
|
||||||
|
if (exitTime >= 3) {
|
||||||
|
Logger.warn('Forcing server close');
|
||||||
|
console.warn('Forcing server close');
|
||||||
|
clearTimeout(timer);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
Logger.info('✌️ Server closed');
|
if (timer) {
|
||||||
process.exit(0);
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
process.exit();
|
||||||
|
}, 15000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
Logger.error('Uncaught exception:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
Logger.error('Unhandled rejection:', reason, promise);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Container } from 'typedi';
|
|||||||
import SockService from '../services/sock';
|
import SockService from '../services/sock';
|
||||||
import config from '../config/index';
|
import config from '../config/index';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { getPlatform } from '../config/util';
|
import { getPlatform, safeJSONParse } from '../config/util';
|
||||||
|
|
||||||
export default async ({ server }: { server: Server }) => {
|
export default async ({ server }: { server: Server }) => {
|
||||||
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||||
@@ -20,7 +20,7 @@ export default async ({ server }: { server: Server }) => {
|
|||||||
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||||
if (data) {
|
if (data) {
|
||||||
const { token = '', tokens = {} } = JSON.parse(data);
|
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||||
if (headerToken === token || tokens[platform] === headerToken) {
|
if (headerToken === token || tokens[platform] === headerToken) {
|
||||||
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
||||||
sockService.addClient(conn);
|
sockService.addClient(conn);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package com.ql.cron;
|
||||||
|
|
||||||
|
service Cron {
|
||||||
|
rpc addCron(AddCronRequest) returns (AddCronResponse);
|
||||||
|
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message ISchedule { string schedule = 1; }
|
||||||
|
|
||||||
|
message ICron {
|
||||||
|
string id = 1;
|
||||||
|
string schedule = 2;
|
||||||
|
string command = 3;
|
||||||
|
repeated ISchedule extra_schedules = 4;
|
||||||
|
string name = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddCronRequest { repeated ICron crons = 1; }
|
||||||
|
|
||||||
|
message AddCronResponse {}
|
||||||
|
|
||||||
|
message DeleteCronRequest { repeated string ids = 1; }
|
||||||
|
|
||||||
|
message DeleteCronResponse {}
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
/* 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 ISchedule {
|
||||||
|
schedule: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICron {
|
||||||
|
id: string;
|
||||||
|
schedule: string;
|
||||||
|
command: string;
|
||||||
|
extraSchedules: ISchedule[];
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddCronRequest {
|
||||||
|
crons: ICron[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddCronResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCronRequest {
|
||||||
|
ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCronResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseISchedule(): ISchedule {
|
||||||
|
return { schedule: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ISchedule = {
|
||||||
|
encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||||
|
if (message.schedule !== "") {
|
||||||
|
writer.uint32(10).string(message.schedule);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: _m0.Reader | Uint8Array, length?: number): ISchedule {
|
||||||
|
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseISchedule();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.schedule = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): ISchedule {
|
||||||
|
return { schedule: isSet(object.schedule) ? String(object.schedule) : "" };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: ISchedule): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
message.schedule !== undefined && (obj.schedule = message.schedule);
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
|
||||||
|
return ISchedule.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
|
||||||
|
const message = createBaseISchedule();
|
||||||
|
message.schedule = object.schedule ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseICron(): ICron {
|
||||||
|
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
for (const v of message.extraSchedules) {
|
||||||
|
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
|
||||||
|
}
|
||||||
|
if (message.name !== "") {
|
||||||
|
writer.uint32(42).string(message.name);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
case 4:
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
case 5:
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.name = 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) : "",
|
||||||
|
extraSchedules: Array.isArray(object?.extraSchedules)
|
||||||
|
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
|
||||||
|
: [],
|
||||||
|
name: isSet(object.name) ? String(object.name) : "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (message.extraSchedules) {
|
||||||
|
obj.extraSchedules = message.extraSchedules.map((e) => e ? ISchedule.toJSON(e) : undefined);
|
||||||
|
} else {
|
||||||
|
obj.extraSchedules = [];
|
||||||
|
}
|
||||||
|
message.name !== undefined && (obj.name = message.name);
|
||||||
|
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 ?? "";
|
||||||
|
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
||||||
|
message.name = object.name ?? "";
|
||||||
|
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,254 @@
|
|||||||
|
/* 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
-6
@@ -1,16 +1,21 @@
|
|||||||
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(
|
||||||
|
`0.0.0.0:${config.cronPort}`,
|
||||||
|
credentials.createInsecure(),
|
||||||
|
);
|
||||||
|
|
||||||
app.get('/api/public/panel/log', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
exec('tail -n 300 ~/.pm2/logs/panel-error.log', (err, stdout, stderr) => {
|
client.check({ service: 'cron' }, (err, response) => {
|
||||||
if (err || stderr) {
|
if (err) {
|
||||||
return res.send({ code: 400, message: (err && err.message) || stderr });
|
return res.status(200).send({ code: 500, error: err });
|
||||||
}
|
}
|
||||||
return res.send({ code: 200, data: stdout });
|
return res.status(200).send({ code: 200, data: response });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,8 +25,11 @@ app
|
|||||||
await require('./loaders/db').default();
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
Logger.debug(`✌️ 公共服务启动成功!`);
|
Logger.debug(`✌️ 公共服务启动成功!`);
|
||||||
|
console.debug(`✌️ 公共服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,56 +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';
|
|
||||||
import { QL_PREFIX, TASK_PREFIX } from './config/const';
|
|
||||||
|
|
||||||
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.startsWith(TASK_PREFIX) &&
|
|
||||||
!command.startsWith(QL_PREFIX)
|
|
||||||
) {
|
|
||||||
command = `${TASK_PREFIX}${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,57 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { AddCronRequest, AddCronResponse } from '../protos/cron';
|
||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { scheduleStacks } from './data';
|
||||||
|
import { runCron } from '../shared/runCron';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
const addCron = (
|
||||||
|
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
|
||||||
|
callback: sendUnaryData<AddCronResponse>,
|
||||||
|
) => {
|
||||||
|
for (const item of call.request.crons) {
|
||||||
|
const { id, schedule, command, extraSchedules, name } = item;
|
||||||
|
if (scheduleStacks.has(id)) {
|
||||||
|
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
schedule,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (extraSchedules?.length) {
|
||||||
|
extraSchedules.forEach(x => {
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
x.schedule,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleStacks.set(id, [
|
||||||
|
nodeSchedule.scheduleJob(id, schedule, async () => {
|
||||||
|
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||||
|
runCron(command, { name, schedule, extraSchedules });
|
||||||
|
}),
|
||||||
|
...(extraSchedules?.length
|
||||||
|
? extraSchedules.map((x) =>
|
||||||
|
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
||||||
|
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||||
|
runCron(command, { name, schedule, extraSchedules });
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: []),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { addCron };
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { credentials } from '@grpc/grpc-js';
|
||||||
|
import {
|
||||||
|
AddCronRequest,
|
||||||
|
AddCronResponse,
|
||||||
|
CronClient,
|
||||||
|
DeleteCronRequest,
|
||||||
|
DeleteCronResponse,
|
||||||
|
} from '../protos/cron';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
private client = new CronClient(
|
||||||
|
`0.0.0.0:${config.cronPort}`,
|
||||||
|
credentials.createInsecure(),
|
||||||
|
);
|
||||||
|
|
||||||
|
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.addCron({ crons: request }, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.delCron({ ids: request }, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Client();
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { ToadScheduler } from 'toad-scheduler';
|
||||||
|
|
||||||
|
export const scheduleStacks = new Map<string, nodeSchedule.Job[]>();
|
||||||
|
|
||||||
|
export const intervalSchedule = new ToadScheduler();
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
|
||||||
|
import { scheduleStacks } from './data';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
const delCron = (
|
||||||
|
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
|
||||||
|
callback: sendUnaryData<DeleteCronResponse>,
|
||||||
|
) => {
|
||||||
|
for (const id of call.request.ids) {
|
||||||
|
if (scheduleStacks.has(id)) {
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][取消定时任务], 任务ID: %s',
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
scheduleStacks.get(id)?.forEach(x => x.cancel());
|
||||||
|
scheduleStacks.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { delCron };
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
||||||
|
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
|
||||||
|
import config from '../config';
|
||||||
|
import { promiseExec } from '../config/util';
|
||||||
|
|
||||||
|
const check = async (
|
||||||
|
call: ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,
|
||||||
|
callback: sendUnaryData<HealthCheckResponse>,
|
||||||
|
) => {
|
||||||
|
switch (call.request.service) {
|
||||||
|
case 'cron':
|
||||||
|
const res = await promiseExec(
|
||||||
|
`curl -s http://0.0.0.0:${config.port}/api/system`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.includes('200')) {
|
||||||
|
return callback(null, { status: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelErrLog = await promiseExec(
|
||||||
|
`tail -n 300 ~/.pm2/logs/panel-error.log`,
|
||||||
|
);
|
||||||
|
const scheduleErrLog = await promiseExec(
|
||||||
|
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
|
||||||
|
);
|
||||||
|
return callback(
|
||||||
|
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`.trim()),
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return callback(null, { status: 1 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { check };
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Server, ServerCredentials } from '@grpc/grpc-js';
|
||||||
|
import { CronService } from '../protos/cron';
|
||||||
|
import { addCron } from './addCron';
|
||||||
|
import { delCron } from './delCron';
|
||||||
|
import { HealthService } from '../protos/health';
|
||||||
|
import { check } from './health';
|
||||||
|
import config from '../config';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
const server = new Server();
|
||||||
|
server.addService(HealthService, { check });
|
||||||
|
server.addService(CronService, { addCron, delCron });
|
||||||
|
server.bindAsync(
|
||||||
|
`0.0.0.0:${config.cronPort}`,
|
||||||
|
ServerCredentials.createInsecure(),
|
||||||
|
(err, port) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
server.start();
|
||||||
|
Logger.debug(`✌️ 定时服务启动成功!`);
|
||||||
|
console.debug(`✌️ 定时服务启动成功!`);
|
||||||
|
process.send?.('ready');
|
||||||
|
},
|
||||||
|
);
|
||||||
+165
-105
@@ -2,19 +2,20 @@ 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 {
|
import { getFileContentByName, fileExist, killTask, getUniqPath, safeJSONParse } from '../config/util';
|
||||||
getFileContentByName,
|
|
||||||
concurrentRun,
|
|
||||||
fileExist,
|
|
||||||
killTask,
|
|
||||||
} from '../config/util';
|
|
||||||
import { promises, existsSync } from 'fs';
|
import { promises, existsSync } from 'fs';
|
||||||
import { Op, where, col as colFn } from 'sequelize';
|
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
||||||
|
import cronClient from '../schedule/client';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
import { spawn } from 'cross-spawn';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import pickBy from 'lodash/pickBy';
|
||||||
|
import omit from 'lodash/omit';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
@@ -22,7 +23,7 @@ export default class CronService {
|
|||||||
|
|
||||||
private isSixCron(cron: Crontab) {
|
private isSixCron(cron: Crontab) {
|
||||||
const { schedule } = cron;
|
const { schedule } = cron;
|
||||||
if (schedule?.split(/ +/).length === 6) {
|
if (Number(schedule?.split(/ +/).length) > 5) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -32,6 +33,11 @@ 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);
|
||||||
|
if (this.isSixCron(doc) || doc.extra_schedules?.length) {
|
||||||
|
await cronClient.addCron([
|
||||||
|
{ name: doc.name || '', id: String(doc.id), schedule: doc.schedule!, command: this.makeCommand(doc), extraSchedules: doc.extra_schedules || [] },
|
||||||
|
]);
|
||||||
|
}
|
||||||
await this.set_crontab();
|
await this.set_crontab();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
@@ -41,8 +47,27 @@ 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 });
|
||||||
|
tab.saved = false;
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
if (doc.isDisabled === 1) {
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
if (this.isSixCron(doc) || doc.extra_schedules?.length) {
|
||||||
|
await cronClient.delCron([String(doc.id)]);
|
||||||
|
}
|
||||||
|
if (this.isSixCron(newDoc) || newDoc.extra_schedules?.length) {
|
||||||
|
await cronClient.addCron([
|
||||||
|
{
|
||||||
|
name: doc.name || '',
|
||||||
|
id: String(newDoc.id),
|
||||||
|
schedule: newDoc.schedule!,
|
||||||
|
command: this.makeCommand(newDoc),
|
||||||
|
extraSchedules: newDoc.extra_schedules || []
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
await this.set_crontab();
|
await this.set_crontab();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
@@ -67,7 +92,7 @@ export default class CronService {
|
|||||||
last_running_time: number;
|
last_running_time: number;
|
||||||
last_execution_time: number;
|
last_execution_time: number;
|
||||||
}) {
|
}) {
|
||||||
const options: any = {
|
let options: any = {
|
||||||
status,
|
status,
|
||||||
pid,
|
pid,
|
||||||
log_path,
|
log_path,
|
||||||
@@ -77,11 +102,18 @@ export default class CronService {
|
|||||||
options.last_running_time = last_running_time;
|
options.last_running_time = last_running_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await CrontabModel.update({ ...options }, { where: { id: ids } });
|
for (const id of ids) {
|
||||||
|
const cron = await this.getDb({ id });
|
||||||
|
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
|
||||||
|
options = omit(options, ['status', 'log_path', 'pid']);
|
||||||
|
}
|
||||||
|
await CrontabModel.update({ ...pickBy(options, (v) => v === 0 || !!v) }, { where: { id } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
await CrontabModel.destroy({ where: { id: ids } });
|
await CrontabModel.destroy({ where: { id: ids } });
|
||||||
|
await cronClient.delCron(ids.map(String));
|
||||||
await this.set_crontab();
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +172,7 @@ export default class CronService {
|
|||||||
case 'In':
|
case 'In':
|
||||||
q[Op.or] = [
|
q[Op.or] = [
|
||||||
{
|
{
|
||||||
[property]: value,
|
[property]: Array.isArray(value) ? value : [value],
|
||||||
},
|
},
|
||||||
property === 'status' && value.includes(2)
|
property === 'status' && value.includes(2)
|
||||||
? { isDisabled: 1 }
|
? { isDisabled: 1 }
|
||||||
@@ -151,7 +183,7 @@ export default class CronService {
|
|||||||
q[Op.and] = [
|
q[Op.and] = [
|
||||||
{
|
{
|
||||||
[property]: {
|
[property]: {
|
||||||
[Op.notIn]: value,
|
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
property === 'status' && value.includes(2)
|
property === 'status' && value.includes(2)
|
||||||
@@ -168,17 +200,13 @@ export default class CronService {
|
|||||||
{
|
{
|
||||||
[operate2]: [
|
[operate2]: [
|
||||||
{ [operate]: `%${value}%` },
|
{ [operate]: `%${value}%` },
|
||||||
{ [operate]: `%${encodeURIComponent(value)}%` },
|
{ [operate]: `%${encodeURI(value)}%` },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[operate2]: [
|
[operate2]: [
|
||||||
where(colFn(property), operate, `%${value}%`),
|
where(colFn(property), operate, `%${value}%`),
|
||||||
where(
|
where(colFn(property), operate, `%${encodeURI(value)}%`),
|
||||||
colFn(property),
|
|
||||||
operate,
|
|
||||||
`%${encodeURIComponent(value)}%`,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -205,7 +233,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;
|
||||||
@@ -213,7 +241,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] = [
|
||||||
@@ -244,8 +272,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(CrontabStatus.disabled)) {
|
||||||
|
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
|
||||||
|
} else {
|
||||||
|
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
q[key] = filterQuery[key];
|
||||||
}
|
}
|
||||||
query[Op.and].push(q);
|
query[Op.and].push(q);
|
||||||
}
|
}
|
||||||
@@ -260,6 +295,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;
|
||||||
@@ -271,9 +319,9 @@ export default class CronService {
|
|||||||
const searchText = params?.searchValue;
|
const searchText = params?.searchValue;
|
||||||
const page = Number(params?.page || '0');
|
const page = Number(params?.page || '0');
|
||||||
const size = Number(params?.size || '0');
|
const size = Number(params?.size || '0');
|
||||||
const viewQuery = JSON.parse(params?.queryString || '{}');
|
const viewQuery = safeJSONParse(params?.queryString);
|
||||||
const filterQuery = JSON.parse(params?.filters || '{}');
|
const filterQuery = safeJSONParse(params?.filters);
|
||||||
const sorterQuery = JSON.parse(params?.sorter || '{}');
|
const sorterQuery = safeJSONParse(params?.sorter);
|
||||||
|
|
||||||
let query: any = {};
|
let query: any = {};
|
||||||
let order = [
|
let order = [
|
||||||
@@ -311,7 +359,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);
|
||||||
}
|
}
|
||||||
@@ -321,10 +369,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[]) {
|
||||||
@@ -334,7 +381,7 @@ export default class CronService {
|
|||||||
try {
|
try {
|
||||||
await killTask(doc.pid);
|
await killTask(doc.pid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.silly(error);
|
this.logger.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,75 +392,71 @@ export default class CronService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runSingle(cronId: number): Promise<number> {
|
private async runSingle(cronId: number): Promise<number | void> {
|
||||||
return new Promise(async (resolve: any) => {
|
return taskLimit.runWithCronLimit(() => {
|
||||||
const cron = await this.getDb({ id: cronId });
|
return new Promise(async (resolve: any) => {
|
||||||
if (cron.status !== CrontabStatus.queued) {
|
const cron = await this.getDb({ id: cronId });
|
||||||
resolve();
|
const params = { name: cron.name, command: cron.command, schedule: cron.schedule, extraSchedules: cron.extra_schedules };
|
||||||
return;
|
if (cron.status !== CrontabStatus.queued) {
|
||||||
}
|
resolve(params);
|
||||||
|
return;
|
||||||
let { id, command, log_path } = cron;
|
|
||||||
const absolutePath = path.resolve(config.logPath, `${log_path}`);
|
|
||||||
const logFileExist = log_path && (await fileExist(absolutePath));
|
|
||||||
|
|
||||||
this.logger.silly('Running job');
|
|
||||||
this.logger.silly('ID: ' + id);
|
|
||||||
this.logger.silly('Original command: ' + command);
|
|
||||||
|
|
||||||
let cmdStr = command;
|
|
||||||
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
|
|
||||||
cmdStr = `${TASK_PREFIX}${cmdStr}`;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
cmdStr.endsWith('.js') ||
|
|
||||||
cmdStr.endsWith('.py') ||
|
|
||||||
cmdStr.endsWith('.pyc') ||
|
|
||||||
cmdStr.endsWith('.sh') ||
|
|
||||||
cmdStr.endsWith('.ts')
|
|
||||||
) {
|
|
||||||
cmdStr = `${cmdStr} now`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
|
|
||||||
|
|
||||||
await CrontabModel.update(
|
|
||||||
{ status: CrontabStatus.running, pid: cp.pid },
|
|
||||||
{ where: { id } },
|
|
||||||
);
|
|
||||||
cp.stderr.on('data', (data) => {
|
|
||||||
if (logFileExist) {
|
|
||||||
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
cp.on('error', (err) => {
|
|
||||||
if (logFileExist) {
|
|
||||||
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('exit', async (code, signal) => {
|
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`);
|
||||||
this.logger.info(
|
|
||||||
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
let { id, command, log_path } = cron;
|
||||||
);
|
const uniqPath = await getUniqPath(command, `${id}`);
|
||||||
});
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
cp.on('close', async (code) => {
|
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||||
|
if (log_path?.split('/')?.every(x => x !== uniqPath)) {
|
||||||
|
fs.mkdirSync(logDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
const logPath = `${uniqPath}/${logTime}.log`;
|
||||||
|
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
||||||
|
|
||||||
|
const cp = spawn(`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron)}`, { shell: '/bin/bash' });
|
||||||
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.idle, pid: undefined },
|
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
|
||||||
{ where: { id } },
|
{ where: { id } },
|
||||||
);
|
);
|
||||||
resolve();
|
cp.stderr.on('data', (data) => {
|
||||||
|
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
|
||||||
|
});
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code) => {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
|
{ where: { id } },
|
||||||
|
);
|
||||||
|
resolve({ ...params, pid: cp.pid, code });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 cronClient.delCron(ids.map(String));
|
||||||
await this.set_crontab();
|
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 } });
|
||||||
|
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||||
|
const sixCron = docs
|
||||||
|
.filter((x) => this.isSixCron(x))
|
||||||
|
.map((doc) => ({
|
||||||
|
name: doc.name || '',
|
||||||
|
id: String(doc.id),
|
||||||
|
schedule: doc.schedule!,
|
||||||
|
command: this.makeCommand(doc),
|
||||||
|
extraSchedules: doc.extra_schedules || []
|
||||||
|
}));
|
||||||
|
await cronClient.addCron(sixCron);
|
||||||
await this.set_crontab();
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +471,7 @@ export default class CronService {
|
|||||||
if (logFileExist) {
|
if (logFileExist) {
|
||||||
return getFileContentByName(`${absolutePath}`);
|
return getFileContentByName(`${absolutePath}`);
|
||||||
} else {
|
} else {
|
||||||
return '任务未运行或运行失败,请尝试手动运行';
|
return '任务未运行';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,44 +495,49 @@ export default class CronService {
|
|||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private make_command(tab: Crontab) {
|
private makeCommand(tab: Crontab) {
|
||||||
if (
|
let command = tab.command.trim();
|
||||||
!tab.command.startsWith(TASK_PREFIX) &&
|
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||||
!tab.command.startsWith(QL_PREFIX)
|
command = `${TASK_PREFIX}${tab.command}`;
|
||||||
) {
|
|
||||||
tab.command = `${TASK_PREFIX}${tab.command}`;
|
|
||||||
}
|
}
|
||||||
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
let commandVariable = `no_tee=true ID=${tab.id} `
|
||||||
|
if (tab.task_before) {
|
||||||
|
commandVariable += `task_before='${tab.task_before.replace(/'/g, "'\\''")
|
||||||
|
.trim()}' `;
|
||||||
|
}
|
||||||
|
if (tab.task_after) {
|
||||||
|
commandVariable += `task_after='${tab.task_after.replace(/'/g, "'\\''")
|
||||||
|
.trim()}' `;
|
||||||
|
}
|
||||||
|
|
||||||
|
const crontab_job_string = `${commandVariable}${command}`;
|
||||||
return crontab_job_string;
|
return crontab_job_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async set_crontab() {
|
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(/ +/);
|
||||||
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
|
if (tab.isDisabled === 1 || _schedule!.length !== 5 || tab.extra_schedules?.length) {
|
||||||
crontab_string += '# ';
|
crontab_string += '# ';
|
||||||
crontab_string += tab.schedule;
|
crontab_string += tab.schedule;
|
||||||
crontab_string += ' ';
|
crontab_string += ' ';
|
||||||
crontab_string += this.make_command(tab);
|
crontab_string += this.makeCommand(tab);
|
||||||
crontab_string += '\n';
|
crontab_string += '\n';
|
||||||
} else {
|
} else {
|
||||||
crontab_string += tab.schedule;
|
crontab_string += tab.schedule;
|
||||||
crontab_string += ' ';
|
crontab_string += ' ';
|
||||||
crontab_string += this.make_command(tab);
|
crontab_string += this.makeCommand(tab);
|
||||||
crontab_string += '\n';
|
crontab_string += '\n';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.silly(crontab_string);
|
|
||||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||||
|
|
||||||
execSync(`crontab ${config.crontabFile}`);
|
execSync(`crontab ${config.crontabFile}`);
|
||||||
exec(`pm2 reload schedule`);
|
|
||||||
await CrontabModel.update({ saved: true }, { where: {} });
|
await CrontabModel.update({ saved: true }, { where: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +575,19 @@ 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) => ({
|
||||||
|
name: doc.name || '',
|
||||||
|
id: String(doc.id),
|
||||||
|
schedule: doc.schedule!,
|
||||||
|
command: this.makeCommand(doc),
|
||||||
|
extraSchedules: doc.extra_schedules || []
|
||||||
|
}));
|
||||||
|
await cronClient.addCron(sixCron);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
minPosition,
|
minPosition,
|
||||||
stepPosition,
|
stepPosition,
|
||||||
} from '../data/env';
|
} from '../data/env';
|
||||||
|
import { FindOptions } from 'sequelize';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronViewService {
|
export default class CronViewService {
|
||||||
@@ -31,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +59,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
+169
-101
@@ -8,12 +8,15 @@ import {
|
|||||||
DependenceTypes,
|
DependenceTypes,
|
||||||
unInstallDependenceCommandTypes,
|
unInstallDependenceCommandTypes,
|
||||||
DependenceModel,
|
DependenceModel,
|
||||||
|
GetDependenceCommandTypes,
|
||||||
|
versionDependenceCommandTypes,
|
||||||
} 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 { promiseExecSuccess } from '../config/util';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class DependenceService {
|
export default class DependenceService {
|
||||||
@@ -24,7 +27,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 +48,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 +62,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 +81,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 +102,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,113 +131,182 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateLog(ids: number[], log: string): Promise<void> {
|
private async updateLog(ids: number[], log: string): Promise<void> {
|
||||||
const doc = await DependenceModel.findOne({ where: { id: ids } });
|
taskLimit.updateDepLog(async () => {
|
||||||
const newLog = doc?.log ? [...doc.log, log] : [log];
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
|
for (const doc of docs) {
|
||||||
|
const newLog = doc?.log ? [...doc.log, log] : [log];
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ log: newLog },
|
||||||
|
{ where: { id: doc.id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public installOrUninstallDependencies(
|
public installOrUninstallDependency(
|
||||||
dependencies: Dependence[],
|
dependency: Dependence,
|
||||||
isInstall: boolean = true,
|
isInstall: boolean = true,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
) {
|
) {
|
||||||
return new Promise(async (resolve) => {
|
return taskLimit.runOneByOne(() => {
|
||||||
if (dependencies.length === 0) {
|
return new Promise(async (resolve) => {
|
||||||
resolve(null);
|
const depIds = [dependency.id!];
|
||||||
return;
|
const status = isInstall
|
||||||
}
|
? DependenceStatus.installing
|
||||||
const socketMessageType = !force
|
: DependenceStatus.removing;
|
||||||
? 'installDependence'
|
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||||
: 'uninstallDependence';
|
|
||||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
|
||||||
const depRunCommand = (
|
|
||||||
isInstall
|
|
||||||
? InstallDependenceCommandTypes
|
|
||||||
: unInstallDependenceCommandTypes
|
|
||||||
)[dependencies[0].type as any];
|
|
||||||
const actionText = isInstall ? '安装' : '删除';
|
|
||||||
const depIds = dependencies.map((x) => x.id) as number[];
|
|
||||||
const startTime = dayjs();
|
|
||||||
|
|
||||||
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
|
const socketMessageType = isInstall
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
? 'installDependence'
|
||||||
)}\n\n`;
|
: 'uninstallDependence';
|
||||||
this.sockService.sendMessage({
|
let depName = dependency.name.trim();
|
||||||
type: socketMessageType,
|
const depRunCommand = (
|
||||||
message,
|
isInstall
|
||||||
references: depIds,
|
? InstallDependenceCommandTypes
|
||||||
});
|
: unInstallDependenceCommandTypes
|
||||||
await this.updateLog(depIds, message);
|
)[dependency.type];
|
||||||
|
const actionText = isInstall ? '安装' : '删除';
|
||||||
|
const startTime = dayjs();
|
||||||
|
|
||||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
|
||||||
|
|
||||||
cp.stdout.on('data', async (data) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: socketMessageType,
|
|
||||||
message: data.toString(),
|
|
||||||
references: depIds,
|
|
||||||
});
|
|
||||||
await this.updateLog(depIds, data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.stderr.on('data', async (data) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: socketMessageType,
|
|
||||||
message: data.toString(),
|
|
||||||
references: depIds,
|
|
||||||
});
|
|
||||||
await this.updateLog(depIds, data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('error', async (err) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: socketMessageType,
|
|
||||||
message: JSON.stringify(err),
|
|
||||||
references: depIds,
|
|
||||||
});
|
|
||||||
await this.updateLog(depIds, JSON.stringify(err));
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('close', async (code) => {
|
|
||||||
const endTime = dayjs();
|
|
||||||
const isSucceed = code === 0;
|
|
||||||
const resultText = isSucceed ? '成功' : '失败';
|
|
||||||
|
|
||||||
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
)}\n\n`;
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: socketMessageType,
|
type: socketMessageType,
|
||||||
message,
|
message,
|
||||||
references: depIds,
|
references: depIds,
|
||||||
});
|
});
|
||||||
await this.updateLog(depIds, message);
|
this.updateLog(depIds, message);
|
||||||
|
|
||||||
let status = null;
|
// 判断是否已经安装过依赖
|
||||||
if (isSucceed) {
|
if (isInstall) {
|
||||||
status = isInstall
|
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
|
||||||
? DependenceStatus.installed
|
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
||||||
: DependenceStatus.removed;
|
let depVersion = '';
|
||||||
} else {
|
if (depName.includes(depVersionStr)) {
|
||||||
status = isInstall
|
const symbolRegx = new RegExp(
|
||||||
? DependenceStatus.installFailed
|
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
|
||||||
: DependenceStatus.removeFailed;
|
);
|
||||||
}
|
const [, _depName, _depVersion] = depName.match(symbolRegx) || [];
|
||||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
if (_depVersion && _depName) {
|
||||||
|
depName = _depName;
|
||||||
|
depVersion = _depVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isNodeDependence = dependency.type === DependenceTypes.nodejs;
|
||||||
|
const isLinuxDependence = dependency.type === DependenceTypes.linux;
|
||||||
|
const isPythonDependence =
|
||||||
|
dependency.type === DependenceTypes.python3;
|
||||||
|
const depInfo = (
|
||||||
|
await promiseExecSuccess(
|
||||||
|
isNodeDependence
|
||||||
|
? `${getCommandPrefix} | grep "${depName}" | head -1`
|
||||||
|
: `${getCommandPrefix} ${depName}`,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.replace(/\s{2,}/, ' ')
|
||||||
|
.replace(/\s+$/, '');
|
||||||
|
|
||||||
// 如果删除依赖成功或者强制删除
|
if (
|
||||||
if ((isSucceed || force) && !isInstall) {
|
depInfo &&
|
||||||
this.removeDb(depIds);
|
((isNodeDependence && depInfo.split(' ')?.[0] === depName) ||
|
||||||
|
(isLinuxDependence &&
|
||||||
|
depInfo.toLocaleLowerCase().includes('installed')) ||
|
||||||
|
isPythonDependence) &&
|
||||||
|
(!depVersion || depInfo.includes(depVersion))
|
||||||
|
) {
|
||||||
|
const endTime = dayjs();
|
||||||
|
const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: _message,
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, _message);
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.installed },
|
||||||
|
{ where: { id: depIds } },
|
||||||
|
);
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(null);
|
const cp = spawn(`${depRunCommand} ${dependency.name.trim()}`, {
|
||||||
|
shell: '/bin/bash',
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stdout.on('data', async (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: data.toString(),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stderr.on('data', async (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: data.toString(),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('error', async (err) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message: JSON.stringify(err),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, JSON.stringify(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code) => {
|
||||||
|
const endTime = dayjs();
|
||||||
|
const isSucceed = code === 0;
|
||||||
|
const resultText = isSucceed ? '成功' : '失败';
|
||||||
|
|
||||||
|
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: socketMessageType,
|
||||||
|
message,
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, message);
|
||||||
|
|
||||||
|
let status = null;
|
||||||
|
if (isSucceed) {
|
||||||
|
status = isInstall
|
||||||
|
? DependenceStatus.installed
|
||||||
|
: DependenceStatus.removed;
|
||||||
|
} else {
|
||||||
|
status = isInstall
|
||||||
|
? DependenceStatus.installFailed
|
||||||
|
: DependenceStatus.removeFailed;
|
||||||
|
}
|
||||||
|
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||||
|
|
||||||
|
// 如果删除依赖成功或者强制删除
|
||||||
|
if ((isSucceed || force) && !isInstall) {
|
||||||
|
this.removeDb(depIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-12
@@ -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,13 +94,17 @@ 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) {
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -197,11 +202,9 @@ export default class EnvService {
|
|||||||
let value = group
|
let value = group
|
||||||
.map((x) => x.value)
|
.map((x) => x.value)
|
||||||
.join('&')
|
.join('&')
|
||||||
.replace(/(\\)[^n]/g, '\\\\')
|
.replace(/'/g, "'\\''")
|
||||||
.replace(/(\\$)/, '\\\\')
|
|
||||||
.replace(/"/g, '\\"')
|
|
||||||
.trim();
|
.trim();
|
||||||
env_string += `export ${key}="${value}"\n`;
|
env_string += `export ${key}='${value}'\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+401
-180
@@ -1,12 +1,12 @@
|
|||||||
import { NotificationInfo } from '../data/notify';
|
|
||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import UserService from './user';
|
|
||||||
import got from 'got';
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import got from 'got';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
import { parseBody, parseHeaders } from '../config/util';
|
import { parseBody, parseHeaders } from '../config/util';
|
||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import UserService from './user';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class NotificationService {
|
export default class NotificationService {
|
||||||
@@ -28,15 +28,17 @@ export default class NotificationService {
|
|||||||
['iGot', this.iGot],
|
['iGot', this.iGot],
|
||||||
['pushPlus', this.pushPlus],
|
['pushPlus', this.pushPlus],
|
||||||
['email', this.email],
|
['email', this.email],
|
||||||
|
['pushMe', this.pushMe],
|
||||||
['webhook', this.webhook],
|
['webhook', this.webhook],
|
||||||
['lark', this.lark],
|
['lark', this.lark],
|
||||||
|
['chronocat', this.chronocat],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
private title = '';
|
private title = '';
|
||||||
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,33 +80,49 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async gotify() {
|
private async gotify() {
|
||||||
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||||
body: `title=${encodeURIComponent(
|
...this.gotOption,
|
||||||
this.title,
|
body: `title=${encodeURIComponent(
|
||||||
)}&message=${encodeURIComponent(
|
this.title,
|
||||||
this.content,
|
)}&message=${encodeURIComponent(
|
||||||
)}&priority=${gotifyPriority}`,
|
this.content,
|
||||||
headers: {
|
)}&priority=${gotifyPriority}`,
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
headers: {
|
||||||
},
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
})
|
},
|
||||||
.json();
|
})
|
||||||
return typeof res.id === 'number';
|
.json();
|
||||||
|
if (typeof res.id === 'number') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async goCqHttpBot() {
|
private async goCqHttpBot() {
|
||||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||||
json: { message: `${this.title}\n${this.content}` },
|
...this.gotOption,
|
||||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
json: { message: `${this.title}\n${this.content}` },
|
||||||
})
|
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||||
.json();
|
})
|
||||||
return res.retcode === 0;
|
.json();
|
||||||
|
if (res.retcode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async serverChan() {
|
private async serverChan() {
|
||||||
@@ -112,63 +130,98 @@ 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`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(url, {
|
||||||
body: `title=${this.title}&desp=${this.content}`,
|
...this.gotOption,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
body: `title=${this.title}&desp=${this.content}`,
|
||||||
})
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
.json();
|
})
|
||||||
return res.errno === 0 || res.data.errno === 0;
|
.json();
|
||||||
|
if (res.errno === 0 || res.data.errno === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pushDeer() {
|
private async pushDeer() {
|
||||||
const { pushDeerKey, pushDeerUrl } = this.params;
|
const { pushDeerKey, pushDeerUrl } = this.params;
|
||||||
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(url, {
|
||||||
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
...this.gotOption,
|
||||||
this.title,
|
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
||||||
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
this.title,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
||||||
})
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
.json();
|
})
|
||||||
return (
|
.json();
|
||||||
res.content.result.length !== undefined && res.content.result.length > 0
|
if (
|
||||||
);
|
res.content.result.length !== undefined &&
|
||||||
|
res.content.result.length > 0
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async chat() {
|
private async chat() {
|
||||||
const { chatUrl, chatToken } = this.params;
|
const { chatUrl, chatToken } = this.params;
|
||||||
const url = `${chatUrl}${chatToken}`;
|
const url = `${chatUrl}${chatToken}`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(url, {
|
||||||
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
...this.gotOption,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
||||||
})
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
.json();
|
})
|
||||||
return res.success;
|
.json();
|
||||||
|
if (res.success) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async bark() {
|
private async bark() {
|
||||||
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
let { barkPush, barkIcon, barkSound, barkGroup, barkLevel, barkUrl } =
|
||||||
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
this.params;
|
||||||
|
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}&level=${barkLevel}&url=${barkUrl}`;
|
||||||
const res: any = await got
|
|
||||||
.get(url, {
|
try {
|
||||||
...this.gotOption,
|
const res: any = await got
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
.get(url, {
|
||||||
})
|
...this.gotOption,
|
||||||
.json();
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
return res.code === 200;
|
})
|
||||||
|
.json();
|
||||||
|
if (res.code === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async telegramBot() {
|
private async telegramBot() {
|
||||||
@@ -200,15 +253,23 @@ export default class NotificationService {
|
|||||||
https: httpsAgent,
|
https: httpsAgent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(url, {
|
||||||
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
...this.gotOption,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||||
agent,
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
agent,
|
||||||
.json();
|
})
|
||||||
return !!res.ok;
|
.json();
|
||||||
|
if (res.ok) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async dingtalkBot() {
|
private async dingtalkBot() {
|
||||||
@@ -222,42 +283,60 @@ 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}`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(url, {
|
||||||
json: {
|
...this.gotOption,
|
||||||
msgtype: 'text',
|
json: {
|
||||||
text: {
|
msgtype: 'text',
|
||||||
content: ` ${this.title}\n\n${this.content}`,
|
text: {
|
||||||
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
.json();
|
||||||
.json();
|
if (res.errcode === 0) {
|
||||||
return 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' } =
|
||||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
this.params;
|
||||||
const res: any = await got
|
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||||
.post(url, {
|
try {
|
||||||
...this.gotOption,
|
const res: any = await got
|
||||||
json: {
|
.post(url, {
|
||||||
msgtype: 'text',
|
...this.gotOption,
|
||||||
text: {
|
json: {
|
||||||
content: ` ${this.title}\n\n${this.content}`,
|
msgtype: 'text',
|
||||||
|
text: {
|
||||||
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
.json();
|
||||||
.json();
|
if (res.errcode === 0) {
|
||||||
return 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,22 +385,30 @@ export default class NotificationService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res: any = await got
|
try {
|
||||||
.post(
|
const res: any = await got
|
||||||
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
.post(
|
||||||
{
|
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
||||||
...this.gotOption,
|
{
|
||||||
json: {
|
...this.gotOption,
|
||||||
touser,
|
json: {
|
||||||
agentid,
|
touser,
|
||||||
safe: '0',
|
agentid,
|
||||||
...options,
|
safe: '0',
|
||||||
|
...options,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
)
|
||||||
)
|
.json();
|
||||||
.json();
|
|
||||||
|
|
||||||
return res.errcode === 0;
|
if (res.errcode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async aibotk() {
|
private async aibotk() {
|
||||||
@@ -353,85 +440,211 @@ export default class NotificationService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(url, {
|
||||||
json: {
|
...this.gotOption,
|
||||||
...json,
|
json: {
|
||||||
},
|
...json,
|
||||||
})
|
},
|
||||||
.json();
|
})
|
||||||
|
.json();
|
||||||
return res.code === 0;
|
if (res.code === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async iGot() {
|
private async iGot() {
|
||||||
const { iGotPushKey } = this.params;
|
const { iGotPushKey } = this.params;
|
||||||
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(url, {
|
||||||
body: `title=${this.title}&content=${this.content}`,
|
...this.gotOption,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
body: `title=${this.title}&content=${this.content}`,
|
||||||
})
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
.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`;
|
||||||
const res: any = await got
|
try {
|
||||||
.post(url, {
|
const res: any = await got
|
||||||
...this.gotOption,
|
.post(url, {
|
||||||
json: {
|
...this.gotOption,
|
||||||
token: `${pushPlusToken}`,
|
json: {
|
||||||
title: `${this.title}`,
|
token: `${pushPlusToken}`,
|
||||||
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
title: `${this.title}`,
|
||||||
topic: `${pushPlusUser || ''}`,
|
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||||
},
|
topic: `${pushPlusUser || ''}`,
|
||||||
})
|
},
|
||||||
.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;
|
||||||
const res: any = await got
|
|
||||||
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, {
|
if (!larkKey.startsWith('http')) {
|
||||||
...this.gotOption,
|
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
|
||||||
json: {
|
}
|
||||||
msg_type: 'text',
|
|
||||||
content: { text: `${this.title}\n\n${this.content}` },
|
try {
|
||||||
},
|
const res: any = await got
|
||||||
headers: { 'Content-Type': 'application/json' },
|
.post(larkKey, {
|
||||||
})
|
...this.gotOption,
|
||||||
.json();
|
json: {
|
||||||
return res.StatusCode === 0;
|
msg_type: 'text',
|
||||||
|
content: { text: `${this.title}\n\n${this.content}` },
|
||||||
|
},
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
if (res.StatusCode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async email() {
|
private async email() {
|
||||||
const { emailPass, emailService, emailUser } = this.params;
|
const { emailPass, emailService, emailUser } = this.params;
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
service: emailService,
|
|
||||||
auth: {
|
|
||||||
user: emailUser,
|
|
||||||
pass: emailPass,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const info = await transporter.sendMail({
|
try {
|
||||||
from: `"青龙快讯" <${emailUser}>`,
|
const transporter = nodemailer.createTransport({
|
||||||
to: `${emailUser}`,
|
service: emailService,
|
||||||
subject: `${this.title}`,
|
auth: {
|
||||||
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
user: emailUser,
|
||||||
});
|
pass: emailPass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
transporter.close();
|
const info = await transporter.sendMail({
|
||||||
|
from: `"青龙快讯" <${emailUser}>`,
|
||||||
|
to: `${emailUser}`,
|
||||||
|
subject: `${this.title}`,
|
||||||
|
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||||
|
});
|
||||||
|
|
||||||
return !!info.messageId;
|
transporter.close();
|
||||||
|
|
||||||
|
if (info.messageId) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(info));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pushMe() {
|
||||||
|
const { pushMeKey } = this.params;
|
||||||
|
try {
|
||||||
|
const res: any = await got.post(
|
||||||
|
`https://push.i-i.me/?push_key=${pushMeKey}`,
|
||||||
|
{
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
title: this.title,
|
||||||
|
content: this.content,
|
||||||
|
},
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.body === 'success') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.body);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async chronocat() {
|
||||||
|
const { chronocatURL, chronocatQQ, chronocatToekn } = this.params;
|
||||||
|
try {
|
||||||
|
const user_ids = chronocatQQ
|
||||||
|
.match(/user_id=(\d+)/g)
|
||||||
|
?.map((match: any) => match.split('=')[1]);
|
||||||
|
const group_ids = chronocatQQ
|
||||||
|
.match(/group_id=(\d+)/g)
|
||||||
|
?.map((match: any) => match.split('=')[1]);
|
||||||
|
|
||||||
|
const url = `${chronocatURL}/api/message/send`;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${chronocatToekn}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [chat_type, ids] of [
|
||||||
|
[1, user_ids],
|
||||||
|
[2, group_ids],
|
||||||
|
]) {
|
||||||
|
if (!ids) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ids: any = ids;
|
||||||
|
for (const chat_id of _ids) {
|
||||||
|
const data = {
|
||||||
|
peer: {
|
||||||
|
chatType: chat_type,
|
||||||
|
peerUin: chat_id,
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
elementType: 1,
|
||||||
|
textElement: {
|
||||||
|
content: `${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const res: any = await got.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: data,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
if (res.body === 'success') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async webhook() {
|
private async webhook() {
|
||||||
@@ -460,8 +673,16 @@ export default class NotificationService {
|
|||||||
allowGetBody: true,
|
allowGetBody: true,
|
||||||
...bodyParam,
|
...bodyParam,
|
||||||
};
|
};
|
||||||
const res = await got(formatUrl, options);
|
try {
|
||||||
return String(res.statusCode).startsWith('20');
|
const res = await got(formatUrl, options);
|
||||||
|
if (String(res.statusCode).startsWith('20')) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatBody(contentType: string, body: any): object {
|
private formatBody(contentType: string, body: any): object {
|
||||||
@@ -484,11 +705,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}%` },
|
||||||
|
|||||||
+85
-70
@@ -1,16 +1,18 @@
|
|||||||
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 taskLimit from '../shared/pLimit';
|
||||||
|
import { spawn } from 'cross-spawn';
|
||||||
|
|
||||||
interface ScheduleTaskType {
|
export interface ScheduleTaskType {
|
||||||
id: number;
|
id: number;
|
||||||
command: string;
|
command: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -40,72 +42,71 @@ export default class ScheduleService {
|
|||||||
|
|
||||||
private maxBuffer = 200 * 1024 * 1024;
|
private maxBuffer = 200 * 1024 * 1024;
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||||
|
|
||||||
async runTask(
|
async runTask(
|
||||||
command: string,
|
command: string,
|
||||||
callbacks: TaskCallbacks = {},
|
callbacks: TaskCallbacks = {},
|
||||||
|
params: {
|
||||||
|
schedule?: string;
|
||||||
|
name?: string;
|
||||||
|
command?: string;
|
||||||
|
},
|
||||||
completionTime: 'start' | 'end' = 'end',
|
completionTime: 'start' | 'end' = 'end',
|
||||||
) {
|
) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return taskLimit.runWithCronLimit(() => {
|
||||||
try {
|
return new Promise(async (resolve, reject) => {
|
||||||
const startTime = dayjs();
|
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
|
||||||
await callbacks.onBefore?.(startTime);
|
|
||||||
|
|
||||||
const cp = spawn(command, { shell: '/bin/bash' });
|
try {
|
||||||
|
const startTime = dayjs();
|
||||||
|
await callbacks.onBefore?.(startTime);
|
||||||
|
|
||||||
// TODO:
|
const cp = spawn(command, { shell: '/bin/bash' });
|
||||||
callbacks.onStart?.(cp, startTime);
|
|
||||||
completionTime === 'start' && resolve(cp.pid);
|
|
||||||
|
|
||||||
cp.stdout.on('data', async (data) => {
|
callbacks.onStart?.(cp, startTime);
|
||||||
await callbacks.onLog?.(data.toString());
|
completionTime === 'start' && resolve(cp.pid);
|
||||||
});
|
|
||||||
|
|
||||||
cp.stderr.on('data', async (data) => {
|
cp.stdout.on('data', async (data) => {
|
||||||
|
await callbacks.onLog?.(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stderr.on('data', async (data) => {
|
||||||
|
this.logger.info(
|
||||||
|
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
command,
|
||||||
|
data.toString(),
|
||||||
|
);
|
||||||
|
await callbacks.onError?.(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('error', async (err) => {
|
||||||
|
this.logger.error(
|
||||||
|
'[panel][创建任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
command,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
await callbacks.onError?.(JSON.stringify(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code) => {
|
||||||
|
const endTime = dayjs();
|
||||||
|
await callbacks.onEnd?.(
|
||||||
|
cp,
|
||||||
|
endTime,
|
||||||
|
endTime.diff(startTime, 'seconds'),
|
||||||
|
);
|
||||||
|
resolve({ ...params, pid: cp.pid, code });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'执行任务 %s 失败,时间:%s, 错误信息:%j',
|
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
command,
|
command,
|
||||||
new Date().toLocaleString(),
|
error,
|
||||||
data.toString(),
|
|
||||||
);
|
);
|
||||||
await callbacks.onError?.(data.toString());
|
await callbacks.onError?.(JSON.stringify(error));
|
||||||
});
|
}
|
||||||
|
});
|
||||||
cp.on('error', async (err) => {
|
|
||||||
this.logger.error(
|
|
||||||
'创建任务 %s 失败,时间:%s, 错误信息:%j',
|
|
||||||
command,
|
|
||||||
new Date().toLocaleString(),
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
await callbacks.onError?.(JSON.stringify(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('exit', async (code, signal) => {
|
|
||||||
this.logger.info(
|
|
||||||
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('close', async (code) => {
|
|
||||||
const endTime = dayjs();
|
|
||||||
await callbacks.onEnd?.(
|
|
||||||
cp,
|
|
||||||
endTime,
|
|
||||||
endTime.diff(startTime, 'seconds'),
|
|
||||||
);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await this.logger.error(
|
|
||||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
|
||||||
command,
|
|
||||||
new Date().toLocaleString(),
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
await callbacks.onError?.(JSON.stringify(error));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ export default class ScheduleService {
|
|||||||
) {
|
) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'[创建cron任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
'[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
|
||||||
_id,
|
_id,
|
||||||
schedule,
|
schedule,
|
||||||
name,
|
name,
|
||||||
@@ -126,19 +127,30 @@ 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, {
|
||||||
|
name,
|
||||||
|
schedule,
|
||||||
|
command,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (runImmediately) {
|
if (runImmediately) {
|
||||||
await this.runTask(command, callbacks);
|
this.runTask(command, callbacks, {
|
||||||
|
name,
|
||||||
|
schedule,
|
||||||
|
command,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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('[panel][取消定时任务], 任务名: %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(
|
||||||
@@ -149,23 +161,23 @@ export default class ScheduleService {
|
|||||||
) {
|
) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
|
'[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s',
|
||||||
_id,
|
_id,
|
||||||
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);
|
name,
|
||||||
|
command,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
'[执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
command,
|
command,
|
||||||
new Date().toLocaleString(),
|
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -180,13 +192,16 @@ 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, {
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
|
this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name);
|
||||||
this.intervalSchedule.removeById(_id);
|
this.intervalSchedule.removeById(_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ export default class ScriptService {
|
|||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
private cronService: CronService,
|
private cronService: CronService,
|
||||||
private scheduleService: ScheduleService,
|
private scheduleService: ScheduleService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
private taskCallbacks(filePath: string): TaskCallbacks {
|
private taskCallbacks(filePath: string): TaskCallbacks {
|
||||||
return {
|
return {
|
||||||
onEnd: async (cp, endTime, diff) => {
|
onEnd: async (cp, endTime, diff) => {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
},
|
},
|
||||||
onError: async (message: string) => {
|
onError: async (message: string) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
@@ -46,6 +46,7 @@ export default class ScriptService {
|
|||||||
const pid = await this.scheduleService.runTask(
|
const pid = await this.scheduleService.runTask(
|
||||||
command,
|
command,
|
||||||
this.taskCallbacks(filePath),
|
this.taskCallbacks(filePath),
|
||||||
|
{ command },
|
||||||
'start',
|
'start',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -53,14 +54,13 @@ export default class ScriptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async stopScript(filePath: string, pid: number) {
|
public async stopScript(filePath: string, pid: number) {
|
||||||
let str = '';
|
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
|
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`) as number;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await killTask(pid);
|
await killTask(pid);
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
|
|
||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { SockMessage } from '../data/sock';
|
|||||||
export default class SockService {
|
export default class SockService {
|
||||||
private clients: Connection[] = [];
|
private clients: Connection[] = [];
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||||
|
|
||||||
public getClients() {
|
public getClients() {
|
||||||
return this.clients;
|
return this.clients;
|
||||||
|
|||||||
+65
-46
@@ -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}${os.EOL}`, {
|
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
mode: '400',
|
mode: '400',
|
||||||
});
|
});
|
||||||
@@ -23,55 +45,39 @@ 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(
|
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
|
||||||
alias: string,
|
|
||||||
host: string,
|
|
||||||
proxy?: string,
|
|
||||||
): string {
|
|
||||||
if (host === 'github.com') {
|
if (host === 'github.com') {
|
||||||
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
|
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa`;
|
||||||
}
|
|
||||||
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
|
|
||||||
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateSshConfig(configs: string[]) {
|
|
||||||
try {
|
|
||||||
for (const config of configs) {
|
|
||||||
fs.appendFileSync(this.sshConfigFilePath, config, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('写入ssh配置文件失败', error);
|
|
||||||
}
|
}
|
||||||
|
const proxyStr = proxy
|
||||||
|
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
|
||||||
|
: '';
|
||||||
|
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
|
||||||
|
this.sshPath,
|
||||||
|
alias,
|
||||||
|
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
||||||
|
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeSshConfig(alias: string) {
|
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);
|
||||||
}
|
}
|
||||||
@@ -84,14 +90,27 @@ export default class SshKeyService {
|
|||||||
proxy?: string,
|
proxy?: string,
|
||||||
): void {
|
): void {
|
||||||
this.generatePrivateKeyFile(alias, key);
|
this.generatePrivateKeyFile(alias, key);
|
||||||
const config = this.generateSingleSshConfig(alias, host, proxy);
|
this.generateSingleSshConfig(alias, host, proxy);
|
||||||
this.removeSshConfig(alias);
|
|
||||||
this.generateSshConfig([config]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeSSHKey(alias: string, host: string, proxy?: string): void {
|
public removeSSHKey(alias: string, host: string, proxy?: string): void {
|
||||||
this.removePrivateKeyFile(alias);
|
this.removePrivateKeyFile(alias);
|
||||||
const config = this.generateSingleSshConfig(alias, host, proxy);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-118
@@ -6,30 +6,30 @@ 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,
|
killTask,
|
||||||
|
handleLogPath,
|
||||||
|
promiseExec,
|
||||||
|
emptyDir,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import { promises, existsSync } from 'fs';
|
import { promises, existsSync } from 'fs';
|
||||||
import { Op } from 'sequelize';
|
import { FindOptions, Op } from 'sequelize';
|
||||||
import path from 'path';
|
import path, { join } from 'path';
|
||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import SshKeyService from './sshKey';
|
import SshKeyService from './sshKey';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LOG_END_SYMBOL } from '../config/const';
|
import { LOG_END_SYMBOL } from '../config/const';
|
||||||
|
import { formatCommand, formatUrl } from '../config/subscription';
|
||||||
|
import { CrontabModel } from '../data/cron';
|
||||||
|
import CrontabService from './cron';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionService {
|
export default class SubscriptionService {
|
||||||
@@ -38,7 +38,8 @@ export default class SubscriptionService {
|
|||||||
private scheduleService: ScheduleService,
|
private scheduleService: ScheduleService,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
private sshKeyService: SshKeyService,
|
private sshKeyService: SshKeyService,
|
||||||
) {}
|
private crontabService: CrontabService,
|
||||||
|
) { }
|
||||||
|
|
||||||
public async list(searchText?: string): Promise<Subscription[]> {
|
public async list(searchText?: string): Promise<Subscription[]> {
|
||||||
let query = {};
|
let query = {};
|
||||||
@@ -46,7 +47,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,71 +69,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,
|
|
||||||
proxy,
|
|
||||||
} = doc;
|
|
||||||
if (type === 'file') {
|
|
||||||
command += `raw "${_url}"`;
|
|
||||||
} else {
|
|
||||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
|
||||||
dependences || ''
|
|
||||||
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}"`;
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
doc.proxy,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.sshKeyService.removeSSHKey(doc.alias, host, doc.proxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -155,28 +105,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 {
|
||||||
@@ -191,7 +122,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`,
|
||||||
);
|
);
|
||||||
@@ -201,7 +132,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 =
|
||||||
@@ -221,14 +152,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 =
|
||||||
@@ -258,12 +189,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}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -273,6 +204,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,8 +213,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,15 +257,29 @@ export default class SubscriptionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[], query: any) {
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
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();
|
||||||
|
|
||||||
|
if (query?.force === true) {
|
||||||
|
const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
|
||||||
|
if (crons?.length) {
|
||||||
|
await this.crontabService.remove(crons.map(x => x.id!))
|
||||||
|
}
|
||||||
|
for (const doc of docs) {
|
||||||
|
const filePath = join(config.scriptPath, doc.alias);
|
||||||
|
emptyDir(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<Subscription> {
|
public async getDb(
|
||||||
|
query: FindOptions<Subscription>['where'],
|
||||||
|
): Promise<Subscription> {
|
||||||
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
|
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as Subscription);
|
return doc && (doc.get({ plain: true }) as Subscription);
|
||||||
}
|
}
|
||||||
@@ -340,10 +289,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[]) {
|
||||||
@@ -353,17 +301,9 @@ export default class SubscriptionService {
|
|||||||
try {
|
try {
|
||||||
await killTask(doc.pid);
|
await killTask(doc.pid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.silly(error);
|
this.logger.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const absolutePath = await this.handleLogPath(doc.log_path as string);
|
|
||||||
|
|
||||||
fs.appendFileSync(
|
|
||||||
`${absolutePath}`,
|
|
||||||
`\n## 执行结束... ${dayjs().format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
)}${LOG_END_SYMBOL}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await SubscriptionModel.update(
|
await SubscriptionModel.update(
|
||||||
@@ -378,28 +318,31 @@ export default class SubscriptionService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = this.formatCommand(subscription);
|
const command = formatCommand(subscription);
|
||||||
|
|
||||||
await this.scheduleService.runTask(
|
this.scheduleService.runTask(command, this.taskCallbacks(subscription), {
|
||||||
command,
|
name: subscription.name,
|
||||||
this.taskCallbacks(subscription),
|
schedule: subscription.schedule,
|
||||||
);
|
command
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -408,7 +351,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+165
-29
@@ -1,15 +1,34 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import {
|
||||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
AuthDataType,
|
||||||
|
AuthInfo,
|
||||||
|
AuthInstance,
|
||||||
|
AuthModel,
|
||||||
|
AuthModelInfo,
|
||||||
|
} 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 { parseContentVersion, parseVersion } from '../config/util';
|
import {
|
||||||
|
getPid,
|
||||||
|
killTask,
|
||||||
|
parseContentVersion,
|
||||||
|
parseVersion,
|
||||||
|
promiseExec,
|
||||||
|
readDirs,
|
||||||
|
} from '../config/util';
|
||||||
|
import { TASK_COMMAND } from '../config/const';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
import tar from 'tar';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import sum from 'lodash/sum';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SystemService {
|
export default class SystemService {
|
||||||
@@ -20,22 +39,22 @@ export default class SystemService {
|
|||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
private scheduleService: ScheduleService,
|
private scheduleService: ScheduleService,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
public async getLogRemoveFrequency() {
|
public async getSystemConfig() {
|
||||||
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
|
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||||
return doc || {};
|
return doc || ({} as AuthInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
private async updateAuthDb(payload: AuthInfo): Promise<AuthInstance> {
|
||||||
await AuthModel.upsert({ ...payload });
|
await AuthModel.upsert({ ...payload });
|
||||||
const doc = await this.getDb({ type: payload.type });
|
const doc = await this.getDb({ type: payload.type });
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<any> {
|
public async getDb(query: any): Promise<AuthInstance> {
|
||||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||||
return doc && (doc.get({ plain: true }) as any);
|
return doc && doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||||
@@ -56,25 +75,30 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateLogRemoveFrequency(frequency: number) {
|
public async updateSystemConfig(info: AuthModelInfo) {
|
||||||
const oDoc = await this.getLogRemoveFrequency();
|
const oDoc = await this.getSystemConfig();
|
||||||
const result = await this.updateAuthDb({
|
const result = await this.updateAuthDb({
|
||||||
...oDoc,
|
...oDoc,
|
||||||
type: AuthDataType.removeLogFrequency,
|
type: AuthDataType.systemConfig,
|
||||||
info: { frequency },
|
info,
|
||||||
});
|
});
|
||||||
const cron = {
|
if (info.logRemoveFrequency) {
|
||||||
id: result.id,
|
const cron = {
|
||||||
name: '删除日志',
|
id: result.id || NaN,
|
||||||
command: `ql rmlog ${frequency}`,
|
name: '删除日志',
|
||||||
};
|
command: `ql rmlog ${info.logRemoveFrequency}`,
|
||||||
await this.scheduleService.cancelIntervalTask(cron);
|
};
|
||||||
if (frequency > 0) {
|
await this.scheduleService.cancelIntervalTask(cron);
|
||||||
this.scheduleService.createIntervalTask(cron, {
|
if (info.logRemoveFrequency > 0) {
|
||||||
days: frequency,
|
this.scheduleService.createIntervalTask(cron, {
|
||||||
});
|
days: info.logRemoveFrequency,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { code: 200, data: { ...cron } };
|
if (info.cronConcurrency) {
|
||||||
|
await taskLimit.setCustomLimit(info.cronConcurrency);
|
||||||
|
}
|
||||||
|
return { code: 200, data: info };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkUpdate() {
|
public async checkUpdate() {
|
||||||
@@ -90,7 +114,7 @@ export default class SystemService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
lastVersionContent = await parseContentVersion(result.body);
|
lastVersionContent = await parseContentVersion(result.body);
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
|
|
||||||
if (!lastVersionContent) {
|
if (!lastVersionContent) {
|
||||||
lastVersionContent = currentVersionContent;
|
lastVersionContent = currentVersionContent;
|
||||||
@@ -136,7 +160,7 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async updateSystem() {
|
public async updateSystem() {
|
||||||
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
const cp = spawn('ql -l update false', { shell: '/bin/bash' });
|
||||||
|
|
||||||
cp.stdout.on('data', (data) => {
|
cp.stdout.on('data', (data) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
@@ -162,6 +186,33 @@ export default class SystemService {
|
|||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async reloadSystem(target: 'system' | 'data') {
|
||||||
|
const cp = spawn(`ql -l reload ${target || ''}`, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
cp.stdout.on('data', (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'reloadSystem',
|
||||||
|
message: data.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stderr.on('data', (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'reloadSystem',
|
||||||
|
message: data.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'reloadSystem',
|
||||||
|
message: JSON.stringify(err),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { code: 200 };
|
||||||
|
}
|
||||||
|
|
||||||
public async notify({ title, content }: { title: string; content: string }) {
|
public async notify({ title, content }: { title: string; content: string }) {
|
||||||
const isSuccess = await this.notificationService.notify(title, content);
|
const isSuccess = await this.notificationService.notify(title, content);
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
@@ -170,4 +221,89 @@ 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,
|
||||||
|
{
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop({ command, pid }: { command: string; pid: number }) {
|
||||||
|
if (!pid && !command) {
|
||||||
|
return { code: 400, message: '参数错误' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid) {
|
||||||
|
await killTask(pid);
|
||||||
|
return { code: 200 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!command.startsWith(TASK_COMMAND)) {
|
||||||
|
command = `${TASK_COMMAND} ${command}`;
|
||||||
|
}
|
||||||
|
const _pid = await getPid(command);
|
||||||
|
if (_pid) {
|
||||||
|
await killTask(_pid);
|
||||||
|
return { code: 200 };
|
||||||
|
} else {
|
||||||
|
return { code: 400, message: '任务未找到' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async exportData(res: Response) {
|
||||||
|
try {
|
||||||
|
await tar.create(
|
||||||
|
{ gzip: true, file: config.dataTgzFile, cwd: config.rootPath },
|
||||||
|
['data'],
|
||||||
|
);
|
||||||
|
res.download(config.dataTgzFile);
|
||||||
|
} catch (error: any) {
|
||||||
|
return res.send({ code: 400, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async importData() {
|
||||||
|
try {
|
||||||
|
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
|
||||||
|
await tar.x({ file: config.dataTgzFile, cwd: config.tmpPath });
|
||||||
|
return { code: 200 };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { code: 400, message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSystemLog(res: Response) {
|
||||||
|
const result = readDirs(config.systemLogPath, config.systemLogPath);
|
||||||
|
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
|
||||||
|
res.set({
|
||||||
|
'Content-Length': sum(logs.map((x) => x.size)),
|
||||||
|
});
|
||||||
|
(function sendFiles(res, fileNames) {
|
||||||
|
if (fileNames.length === 0) {
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLog = fileNames.shift();
|
||||||
|
if (currentLog) {
|
||||||
|
const currentFileStream = fs.createReadStream(
|
||||||
|
path.join(config.systemLogPath, currentLog.title),
|
||||||
|
);
|
||||||
|
currentFileStream.on('end', () => {
|
||||||
|
sendFiles(res, fileNames);
|
||||||
|
});
|
||||||
|
currentFileStream.pipe(res, { end: false });
|
||||||
|
}
|
||||||
|
})(res, logs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-16
@@ -1,16 +1,27 @@
|
|||||||
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,
|
||||||
|
safeJSONParse,
|
||||||
|
} 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';
|
||||||
import { authenticator } from '@otplib/preset-default';
|
import { authenticator } from '@otplib/preset-default';
|
||||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
import {
|
||||||
|
AuthDataType,
|
||||||
|
AuthInfo,
|
||||||
|
AuthModel,
|
||||||
|
AuthModelInfo,
|
||||||
|
LoginStatus,
|
||||||
|
} from '../data/auth';
|
||||||
import { NotificationInfo } from '../data/notify';
|
import { 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';
|
||||||
|
|
||||||
@@ -183,12 +194,14 @@ export default class UserService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLoginLog(): Promise<AuthInfo[]> {
|
public async getLoginLog(): Promise<Array<AuthModelInfo | undefined>> {
|
||||||
const docs = await AuthModel.findAll({
|
const docs = await AuthModel.findAll({
|
||||||
where: { type: AuthDataType.loginLog },
|
where: { type: AuthDataType.loginLog },
|
||||||
});
|
});
|
||||||
if (docs && docs.length > 0) {
|
if (docs && docs.length > 0) {
|
||||||
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
|
const result = docs.sort(
|
||||||
|
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
|
||||||
|
);
|
||||||
if (result.length > 100) {
|
if (result.length > 100) {
|
||||||
await AuthModel.destroy({
|
await AuthModel.destroy({
|
||||||
where: { id: result[result.length - 1].id },
|
where: { id: result[result.length - 1].id },
|
||||||
@@ -205,17 +218,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 +252,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() {
|
||||||
@@ -311,7 +328,7 @@ export default class UserService {
|
|||||||
|
|
||||||
private getAuthInfo() {
|
private getAuthInfo() {
|
||||||
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
return JSON.parse(content || '{}');
|
return safeJSONParse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateAuthInfo(authInfo: any, info: any) {
|
private updateAuthInfo(authInfo: any, info: any) {
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import PQueue, { QueueAddOptions } from 'p-queue-cjs';
|
||||||
|
import os from 'os';
|
||||||
|
import { AuthDataType, AuthModel } from '../data/auth';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
class TaskLimit {
|
||||||
|
private oneLimit = new PQueue({ concurrency: 1 });
|
||||||
|
private updateLogLimit = new PQueue({ concurrency: 1 });
|
||||||
|
private cronLimit = new PQueue({ concurrency: Math.max(os.cpus().length, 4) });
|
||||||
|
|
||||||
|
get cronLimitActiveCount() {
|
||||||
|
return this.cronLimit.pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
get cronLimitPendingCount() {
|
||||||
|
return this.cronLimit.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.setCustomLimit();
|
||||||
|
this.handleEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleEvents() {
|
||||||
|
this.cronLimit.on('add', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
this.cronLimit.on('active', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][开始处理任务] 运行中任务数: ${this.cronLimitActiveCount + 1}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
this.cronLimit.on('completed', (param) => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('error', error => {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('next', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务处理结束] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('idle', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务队列] 空闲中...`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setCustomLimit(limit?: number) {
|
||||||
|
if (limit) {
|
||||||
|
this.cronLimit.concurrency = limit;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await AuthModel.sync();
|
||||||
|
const doc = await AuthModel.findOne({
|
||||||
|
where: { type: AuthDataType.systemConfig },
|
||||||
|
});
|
||||||
|
if (doc?.info?.cronConcurrency) {
|
||||||
|
this.cronLimit.concurrency = doc.info.cronConcurrency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runWithCronLimit<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
||||||
|
return this.cronLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public runOneByOne<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
||||||
|
return this.oneLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateDepLog<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
||||||
|
return this.updateLogLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new TaskLimit();
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { spawn } from 'cross-spawn';
|
||||||
|
import taskLimit from './pLimit';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
export function runCron(cmd: string, options?: { schedule: string; extraSchedules: Array<{ schedule: string }>; name: string }): Promise<number | void> {
|
||||||
|
return taskLimit.runWithCronLimit(() => {
|
||||||
|
return new Promise(async (resolve: any) => {
|
||||||
|
Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, command: cmd })}`);
|
||||||
|
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
cp.stderr.on('data', (data) => {
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
cmd,
|
||||||
|
data.toString(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
Logger.error(
|
||||||
|
'[schedule][创建任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
cmd,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', async (code) => {
|
||||||
|
resolve({ ...options, command: cmd, pid: cp.pid, code });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+57
-51
@@ -1,11 +1,11 @@
|
|||||||
FROM python:3.10-alpine as builder
|
FROM python:3.10-alpine as builder
|
||||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& apk update \
|
&& apk update \
|
||||||
&& apk add nodejs npm \
|
&& apk add nodejs npm git \
|
||||||
&& npm i -g pnpm \
|
&& npm i -g pnpm@8.3.1 \
|
||||||
&& cd /tmp/build \
|
&& cd /tmp/build \
|
||||||
&& pnpm install --prod
|
&& pnpm --registry https://registry.npmmirror.com install --prod
|
||||||
|
|
||||||
FROM python:3.10-alpine
|
FROM python:3.10-alpine
|
||||||
|
|
||||||
@@ -15,58 +15,64 @@ ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
|||||||
ARG QL_BRANCH=develop
|
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 \
|
||||||
SHELL=/bin/bash \
|
LANG=C.UTF-8 \
|
||||||
PS1="\u@\h:\w \$ " \
|
SHELL=/bin/bash \
|
||||||
QL_DIR=/ql \
|
PS1="\u@\h:\w \$ " \
|
||||||
QL_BRANCH=${QL_BRANCH}
|
QL_DIR=/ql \
|
||||||
|
QL_BRANCH=${QL_BRANCH}
|
||||||
|
|
||||||
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 \
|
tzdata \
|
||||||
tzdata \
|
perl \
|
||||||
perl \
|
openssl \
|
||||||
openssl \
|
nginx \
|
||||||
nginx \
|
nodejs \
|
||||||
nodejs \
|
jq \
|
||||||
jq \
|
openssh \
|
||||||
openssh \
|
procps \
|
||||||
npm \
|
netcat-openbsd \
|
||||||
&& rm -rf /var/cache/apk/* \
|
npm \
|
||||||
&& apk update \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
&& apk update \
|
||||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||||
&& git config --global user.name "qinglong" \
|
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||||
&& git config --global http.postBuffer 524288000 \
|
&& git config --global user.name "qinglong" \
|
||||||
&& npm install -g pnpm \
|
&& git config --global http.postBuffer 524288000 \
|
||||||
&& pnpm add -g pm2 ts-node typescript tslib \
|
&& npm install -g pnpm@8.3.1 \
|
||||||
&& rm -rf /root/.pnpm-store \
|
&& cd && pnpm config set registry https://registry.npmmirror.com \
|
||||||
&& rm -rf /root/.local/share/pnpm/store \
|
&& pnpm add -g pm2 tsx \
|
||||||
&& rm -rf /root/.cache \
|
&& rm -rf /root/.pnpm-store \
|
||||||
&& rm -rf /root/.npm
|
&& rm -rf /root/.local/share/pnpm/store \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& rm -rf /root/.npm
|
||||||
|
|
||||||
ARG SOURCE_COMMIT
|
ARG SOURCE_COMMIT
|
||||||
RUN git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
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 \
|
||||||
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
&& git clone --depth=1 -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/
|
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
WORKDIR ${QL_DIR}
|
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
|
||||||
|
|||||||
+23
-28
@@ -4,57 +4,52 @@ dir_shell=/ql/shell
|
|||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
link_shell
|
link_shell
|
||||||
|
|
||||||
export isFirstStartServer=true
|
|
||||||
|
|
||||||
echo -e "======================1. 检测配置文件========================\n"
|
echo -e "======================1. 检测配置文件========================\n"
|
||||||
make_dir /etc/nginx/conf.d
|
make_dir /etc/nginx/conf.d
|
||||||
make_dir /run/nginx
|
make_dir /run/nginx
|
||||||
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"
|
||||||
update_depend
|
patch_version
|
||||||
|
if [[ $PipMirror ]]; then
|
||||||
|
pip3 config set global.index-url $PipMirror
|
||||||
|
fi
|
||||||
|
current_npm_registry=$(cd && pnpm config get registry)
|
||||||
|
is_equal_registry=$(echo $current_npm_registry | grep "${NpmMirror}")
|
||||||
|
if [[ "$is_equal_registry" == "" ]]; then
|
||||||
|
cd && pnpm config set registry $NpmMirror
|
||||||
|
pnpm install -g
|
||||||
|
fi
|
||||||
|
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
|
||||||
|
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||||
|
npm_install_2 $dir_scripts
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo -e "======================3. 启动nginx========================\n"
|
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
|
||||||
|
|||||||
+13
-8
@@ -12,32 +12,35 @@ map $http_upgrade $connection_upgrade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 5700;
|
IPV4_CONFIG
|
||||||
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)$ {
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ http {
|
|||||||
|
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
|
|
||||||
client_max_body_size 20m;
|
client_max_body_size 4096m;
|
||||||
client_body_buffer_size 20m;
|
client_body_buffer_size 20m;
|
||||||
|
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"watch": ["back", ".env"],
|
"watch": ["back", ".env"],
|
||||||
"ext": "js,ts,json",
|
"ext": "js,ts,json",
|
||||||
"ignore": ["src/**/*.spec.ts"],
|
"exec": "ts-node -P tsconfig.back.json ./back/app.ts"
|
||||||
"exec": "ts-node --transpile-only ./back/app.ts"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-12
@@ -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 --transpile-only ./back/public.ts",
|
"start:public": "ts-node -P tsconfig.back.json ./back/public.ts",
|
||||||
|
"start:rpc": "ts-node -P tsconfig.back.json ./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",
|
||||||
@@ -53,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",
|
||||||
@@ -61,14 +64,17 @@
|
|||||||
"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": "^7.0.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": "^1.2.0",
|
||||||
|
"http-proxy-middleware": "^2.0.6",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
@@ -77,28 +83,34 @@
|
|||||||
"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-cjs": "7.3.4",
|
||||||
|
"protobufjs": "^7.2.3",
|
||||||
"pstree.remy": "^1.1.8",
|
"pstree.remy": "^1.1.8",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sequelize": "^6.25.5",
|
"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.1.2",
|
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
|
||||||
|
"tar": "^6.1.15",
|
||||||
"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",
|
||||||
"winston": "^3.6.0",
|
"winston": "^3.6.0",
|
||||||
|
"winston-daily-rotate-file": "^4.7.1",
|
||||||
"yargs": "^17.3.1"
|
"yargs": "^17.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^4.7.0",
|
"@ant-design/icons": "^4.7.0",
|
||||||
"@ant-design/pro-layout": "6.38.22",
|
"@ant-design/pro-layout": "6.38.22",
|
||||||
"@monaco-editor/react": "4.4.6",
|
"@monaco-editor/react": "4.2.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@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/file-saver": "^2.0.5",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@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",
|
||||||
@@ -114,15 +126,20 @@
|
|||||||
"@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/tar": "^6.1.5",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@umijs/max": "^4.0.42",
|
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||||
|
"@uiw/react-codemirror": "^4.21.9",
|
||||||
|
"@umijs/max": "^4.0.72",
|
||||||
"@umijs/ssr-darkreader": "^4.9.45",
|
"@umijs/ssr-darkreader": "^4.9.45",
|
||||||
|
"ahooks": "^3.7.8",
|
||||||
"ansi-to-react": "^6.1.6",
|
"ansi-to-react": "^6.1.6",
|
||||||
"antd": "^4.24.7",
|
"antd": "^4.24.8",
|
||||||
"antd-img-crop": "^4.2.3",
|
"antd-img-crop": "^4.2.3",
|
||||||
"codemirror": "^5.65.2",
|
"axios": "^1.4.0",
|
||||||
"compression-webpack-plugin": "9.2.0",
|
"compression-webpack-plugin": "9.2.0",
|
||||||
"concurrently": "^7.0.0",
|
"concurrently": "^7.0.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"monaco-editor": "0.33.0",
|
"monaco-editor": "0.33.0",
|
||||||
"nodemon": "^2.0.15",
|
"nodemon": "^2.0.15",
|
||||||
@@ -131,21 +148,23 @@
|
|||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
"rc-tween-one": "^3.0.6",
|
"rc-tween-one": "^3.0.6",
|
||||||
|
"rc-virtual-list": "3.5.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-codemirror2": "^7.2.1",
|
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "^14.0.2",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^14.0.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-intl-universal": "^2.6.21",
|
||||||
"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.8.4",
|
"tsx": "^3.12.3",
|
||||||
"umi-request": "^1.4.0",
|
"typescript": "5.2.2",
|
||||||
"vh-check": "^2.0.5",
|
"vh-check": "^2.0.5",
|
||||||
"virtuallist-antd": "^0.7.6",
|
"virtualizedtableforantd4": "1.3.0",
|
||||||
"webpack": "^5.70.0",
|
"webpack": "^5.70.0",
|
||||||
"yorkie": "^2.0.0"
|
"yorkie": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6553
-5443
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 |
+20
-11
@@ -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"
|
||||||
|
|
||||||
@@ -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 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
|
||||||
@@ -100,6 +91,10 @@ export TG_API_HOST=""
|
|||||||
export DD_BOT_TOKEN=""
|
export DD_BOT_TOKEN=""
|
||||||
export DD_BOT_SECRET=""
|
export DD_BOT_SECRET=""
|
||||||
|
|
||||||
|
## 企业微信反向代理地址
|
||||||
|
## (环境变量名 QYWX_ORIGIN)
|
||||||
|
export QYWX_ORIGIN=""
|
||||||
|
|
||||||
## 5. 企业微信机器人
|
## 5. 企业微信机器人
|
||||||
## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
|
## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
|
||||||
## 下方填写密钥,企业微信推送 webhook 后面的 key
|
## 下方填写密钥,企业微信推送 webhook 后面的 key
|
||||||
@@ -173,4 +168,18 @@ export SMTP_PASSWORD=""
|
|||||||
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
|
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
|
||||||
export SMTP_NAME=""
|
export SMTP_NAME=""
|
||||||
|
|
||||||
|
## 15. PushMe
|
||||||
|
## 官方说明文档:https://push.i-i.me/
|
||||||
|
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
|
||||||
|
export PUSHME_KEY=""
|
||||||
|
|
||||||
|
## 13. CHRONOCAT
|
||||||
|
## CHRONOCAT_URL 推送 http://127.0.0.1:16530
|
||||||
|
## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
|
||||||
|
## CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
|
||||||
|
## CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
|
||||||
|
export CHRONOCAT_URL=""
|
||||||
|
export CHRONOCAT_QQ="" #
|
||||||
|
export CHRONOCAT_TOKEN=""
|
||||||
|
|
||||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||||
|
|||||||
+339
-5
File diff suppressed because one or more lines are too long
+294
-32
@@ -40,6 +40,8 @@ push_config = {
|
|||||||
'BARK_GROUP': '', # bark 推送分组
|
'BARK_GROUP': '', # bark 推送分组
|
||||||
'BARK_SOUND': '', # bark 推送声音
|
'BARK_SOUND': '', # bark 推送声音
|
||||||
'BARK_ICON': '', # bark 推送图标
|
'BARK_ICON': '', # bark 推送图标
|
||||||
|
'BARK_LEVEL': '', # bark 推送时效性
|
||||||
|
'BARK_URL': '', # bark 推送跳转URL
|
||||||
|
|
||||||
'CONSOLE': True, # 控制台输出
|
'CONSOLE': True, # 控制台输出
|
||||||
|
|
||||||
@@ -76,6 +78,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': '', # 企业微信机器人
|
||||||
@@ -96,6 +100,18 @@ push_config = {
|
|||||||
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
|
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
|
||||||
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
||||||
|
|
||||||
|
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
|
||||||
|
|
||||||
|
'CHRONOCAT_QQ': '', # qq号
|
||||||
|
'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token
|
||||||
|
'CHRONOCAT_URL': '', # CHRONOCAT的url地址
|
||||||
|
|
||||||
|
'WEBHOOK_URL': '', # 自定义通知 请求地址
|
||||||
|
'WEBHOOK_BODY': '', # 自定义通知 请求体
|
||||||
|
'WEBHOOK_HEADERS': '', # 自定义通知 请求头
|
||||||
|
'WEBHOOK_METHOD': '', # 自定义通知 请求方法
|
||||||
|
'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type
|
||||||
}
|
}
|
||||||
notify_function = []
|
notify_function = []
|
||||||
# fmt: on
|
# fmt: on
|
||||||
@@ -126,6 +142,8 @@ def bark(title: str, content: str) -> None:
|
|||||||
"BARK_GROUP": "group",
|
"BARK_GROUP": "group",
|
||||||
"BARK_SOUND": "sound",
|
"BARK_SOUND": "sound",
|
||||||
"BARK_ICON": "icon",
|
"BARK_ICON": "icon",
|
||||||
|
"BARK_LEVEL": "level",
|
||||||
|
"BARK_URL": "url",
|
||||||
}
|
}
|
||||||
params = ""
|
params = ""
|
||||||
for pair in filter(
|
for pair in filter(
|
||||||
@@ -220,7 +238,7 @@ def go_cqhttp(title: str, content: str) -> None:
|
|||||||
print("go-cqhttp 推送失败!")
|
print("go-cqhttp 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
def gotify(title:str,content:str) -> None:
|
def gotify(title: str, content: str) -> None:
|
||||||
"""
|
"""
|
||||||
使用 gotify 推送消息。
|
使用 gotify 推送消息。
|
||||||
"""
|
"""
|
||||||
@@ -230,8 +248,12 @@ 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 = {
|
||||||
response = requests.post(url,data=data).json()
|
"title": title,
|
||||||
|
"message": content,
|
||||||
|
"priority": push_config.get("GOTIFY_PRIORITY"),
|
||||||
|
}
|
||||||
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if response.get("id"):
|
if response.get("id"):
|
||||||
print("gotify 推送成功!")
|
print("gotify 推送成功!")
|
||||||
@@ -269,10 +291,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:
|
||||||
@@ -289,8 +311,13 @@ 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"):
|
if push_config.get("DEER_URL"):
|
||||||
url = push_config.get("DEER_URL")
|
url = push_config.get("DEER_URL")
|
||||||
|
|
||||||
@@ -310,7 +337,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)
|
||||||
|
|
||||||
@@ -320,7 +347,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+ 推送消息。
|
||||||
@@ -345,7 +371,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()
|
||||||
@@ -416,9 +441,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,
|
||||||
@@ -429,8 +457,7 @@ class WeCom:
|
|||||||
|
|
||||||
def send_text(self, message, touser="@all"):
|
def send_text(self, message, touser="@all"):
|
||||||
send_url = (
|
send_url = (
|
||||||
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
|
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||||
+ self.get_access_token()
|
|
||||||
)
|
)
|
||||||
send_values = {
|
send_values = {
|
||||||
"touser": touser,
|
"touser": touser,
|
||||||
@@ -446,8 +473,7 @@ class WeCom:
|
|||||||
|
|
||||||
def send_mpnews(self, title, message, media_id, touser="@all"):
|
def send_mpnews(self, title, message, media_id, touser="@all"):
|
||||||
send_url = (
|
send_url = (
|
||||||
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
|
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||||
+ self.get_access_token()
|
|
||||||
)
|
)
|
||||||
send_values = {
|
send_values = {
|
||||||
"touser": touser,
|
"touser": touser,
|
||||||
@@ -481,7 +507,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(
|
||||||
@@ -543,24 +573,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"}
|
||||||
@@ -576,24 +610,225 @@ def smtp(title: str, content: str) -> None:
|
|||||||
"""
|
"""
|
||||||
使用 SMTP 邮件 推送消息。
|
使用 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"):
|
if (
|
||||||
print("SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送")
|
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
|
return
|
||||||
print("SMTP 邮件 服务启动")
|
print("SMTP 邮件 服务启动")
|
||||||
|
|
||||||
message = MIMEText(content, 'plain', 'utf-8')
|
message = MIMEText(content, "plain", "utf-8")
|
||||||
message['From'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
|
message["From"] = formataddr(
|
||||||
message['To'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
|
(
|
||||||
message['Subject'] = Header(title, 'utf-8')
|
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:
|
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 = (
|
||||||
smtp_server.login(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD"))
|
smtplib.SMTP_SSL(push_config.get("SMTP_SERVER"))
|
||||||
smtp_server.sendmail(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_EMAIL"), message.as_bytes())
|
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()
|
smtp_server.close()
|
||||||
print("SMTP 邮件 推送成功!")
|
print("SMTP 邮件 推送成功!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'SMTP 邮件 推送失败!{e}')
|
print(f"SMTP 邮件 推送失败!{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def pushme(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 PushMe 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("PUSHME_KEY"):
|
||||||
|
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("PushMe 服务启动")
|
||||||
|
|
||||||
|
url = f'https://push.i-i.me/?push_key={push_config.get("PUSHME_KEY")}'
|
||||||
|
data = {
|
||||||
|
"title": title,
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
response = requests.post(url, data=data)
|
||||||
|
|
||||||
|
if response.status_code == 200 and response.text == "success":
|
||||||
|
print("PushMe 推送成功!")
|
||||||
|
else:
|
||||||
|
print(f"PushMe 推送失败!{response.status_code} {response.text}")
|
||||||
|
|
||||||
|
|
||||||
|
def chronocat(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 CHRONOCAT 推送消息。
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
not push_config.get("CHRONOCAT_URL")
|
||||||
|
or not push_config.get("CHRONOCAT_QQ")
|
||||||
|
or not push_config.get("CHRONOCAT_TOKEN")
|
||||||
|
):
|
||||||
|
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("CHRONOCAT 服务启动")
|
||||||
|
|
||||||
|
user_ids = re.findall(r"user_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
|
||||||
|
group_ids = re.findall(r"group_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
|
||||||
|
|
||||||
|
url = f'{push_config.get("CHRONOCAT_URL")}/api/message/send'
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f'Bearer {push_config.get("CHRONOCAT_TOKEN")}',
|
||||||
|
}
|
||||||
|
|
||||||
|
for chat_type, ids in [(1, user_ids), (2, group_ids)]:
|
||||||
|
if not ids:
|
||||||
|
continue
|
||||||
|
for chat_id in ids:
|
||||||
|
data = {
|
||||||
|
"peer": {"chatType": chat_type, "peerUin": chat_id},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"elementType": 1,
|
||||||
|
"textElement": {"content": f"{title}\n\n{content}"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(data))
|
||||||
|
if response.status_code == 200:
|
||||||
|
if chat_type == 1:
|
||||||
|
print(f"QQ个人消息:{ids}推送成功!")
|
||||||
|
else:
|
||||||
|
print(f"QQ群消息:{ids}推送成功!")
|
||||||
|
else:
|
||||||
|
if chat_type == 1:
|
||||||
|
print(f"QQ个人消息:{ids}推送失败!")
|
||||||
|
else:
|
||||||
|
print(f"QQ群消息:{ids}推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_headers(headers):
|
||||||
|
if not headers:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
parsed = {}
|
||||||
|
lines = headers.split("\n")
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
i = line.find(":")
|
||||||
|
if i == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = line[:i].strip().lower()
|
||||||
|
val = line[i + 1 :].strip()
|
||||||
|
parsed[key] = parsed.get(key, "") + ", " + val if key in parsed else val
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def parse_body(body, content_type):
|
||||||
|
if not body:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
parsed = {}
|
||||||
|
lines = body.split("\n")
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
i = line.find(":")
|
||||||
|
if i == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = line[:i].strip().lower()
|
||||||
|
val = line[i + 1 :].strip()
|
||||||
|
|
||||||
|
if not key or key in parsed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
json_value = json.loads(val)
|
||||||
|
parsed[key] = json_value
|
||||||
|
except:
|
||||||
|
parsed[key] = val
|
||||||
|
|
||||||
|
if content_type == "application/x-www-form-urlencoded":
|
||||||
|
data = urlencode(parsed, doseq=True)
|
||||||
|
return data
|
||||||
|
|
||||||
|
if content_type == "application/json":
|
||||||
|
data = json.dumps(parsed)
|
||||||
|
return data
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def format_notify_content(url, body, title, content):
|
||||||
|
if "$title" not in url and "$title" not in body:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
formatted_url = url.replace("$title", urllib.parse.quote_plus(title)).replace(
|
||||||
|
"$content", urllib.parse.quote_plus(content)
|
||||||
|
)
|
||||||
|
formatted_body = body.replace("$title", title).replace("$content", content)
|
||||||
|
|
||||||
|
return formatted_url, formatted_body
|
||||||
|
|
||||||
|
|
||||||
|
def custom_notify(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 自定义通知 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
|
||||||
|
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("自定义通知服务启动")
|
||||||
|
|
||||||
|
WEBHOOK_URL = push_config.get("WEBHOOK_URL")
|
||||||
|
WEBHOOK_METHOD = push_config.get("WEBHOOK_METHOD")
|
||||||
|
WEBHOOK_CONTENT_TYPE = push_config.get("WEBHOOK_CONTENT_TYPE")
|
||||||
|
WEBHOOK_BODY = push_config.get("WEBHOOK_BODY")
|
||||||
|
WEBHOOK_HEADERS = push_config.get("WEBHOOK_HEADERS")
|
||||||
|
|
||||||
|
formatUrl, formatBody = format_notify_content(
|
||||||
|
WEBHOOK_URL, WEBHOOK_BODY, title, content
|
||||||
|
)
|
||||||
|
|
||||||
|
if not formatUrl and not formatBody:
|
||||||
|
print("请求头或者请求体中必须包含 $title 和 $content")
|
||||||
|
return
|
||||||
|
|
||||||
|
headers = parse_headers(WEBHOOK_HEADERS)
|
||||||
|
body = parse_body(formatBody, WEBHOOK_CONTENT_TYPE)
|
||||||
|
response = requests.request(
|
||||||
|
method=WEBHOOK_METHOD, url=formatUrl, headers=headers, timeout=15, data=body
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("自定义通知推送成功!")
|
||||||
|
else:
|
||||||
|
print(f"自定义通知推送失败!{response.status_code} {response.text}")
|
||||||
|
|
||||||
|
|
||||||
def one() -> str:
|
def one() -> str:
|
||||||
@@ -636,10 +871,30 @@ 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"):
|
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)
|
notify_function.append(smtp)
|
||||||
|
if push_config.get("PUSHME_KEY"):
|
||||||
|
notify_function.append(pushme)
|
||||||
|
if (
|
||||||
|
push_config.get("CHRONOCAT_URL")
|
||||||
|
and push_config.get("CHRONOCAT_QQ")
|
||||||
|
and push_config.get("CHRONOCAT_TOKEN")
|
||||||
|
):
|
||||||
|
notify_function.append(chronocat)
|
||||||
|
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
|
||||||
|
notify_function.append(custom_notify)
|
||||||
|
|
||||||
|
|
||||||
def send(title: str, content: str) -> None:
|
def send(title: str, content: str) -> None:
|
||||||
@@ -647,6 +902,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 ""
|
||||||
|
|||||||
@@ -3,13 +3,8 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.0.0",
|
|
||||||
"download": "^8.0.0",
|
|
||||||
"got": "^11.5.1",
|
"got": "^11.5.1",
|
||||||
"http-server": "^0.12.3",
|
|
||||||
"nodemailer": "^6.8.0",
|
"nodemailer": "^6.8.0",
|
||||||
"qrcode-terminal": "^0.12.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"
|
||||||
|
|||||||
+71
-33
@@ -1,15 +1,24 @@
|
|||||||
#!/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
|
if [[ -f $file_auth_token ]]; then
|
||||||
token=$(cat $file_auth_token | jq -r .value)
|
token=$(cat $file_auth_token | jq -r .value)
|
||||||
else
|
local expiration=$(cat $file_auth_token | jq -r .expiration)
|
||||||
local token_command="ts-node-transpile-only ${dir_root}/back/token.ts"
|
local currentTimeStamp=$(date +%s)
|
||||||
local token_file="${dir_root}static/build/token.js"
|
if [[ $currentTimeStamp -ge $expiration ]]; then
|
||||||
if [[ -f $token_file ]]; then
|
create_token
|
||||||
token_command="node ${token_file}"
|
|
||||||
fi
|
fi
|
||||||
token=$(eval "$token_command")
|
else
|
||||||
|
create_token
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,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=$(
|
||||||
@@ -34,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
|
||||||
@@ -54,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=$(
|
||||||
@@ -73,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
|
||||||
@@ -88,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=$(
|
||||||
@@ -105,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
|
||||||
@@ -115,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" \
|
||||||
@@ -130,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
|
||||||
@@ -160,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" \
|
||||||
@@ -184,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
+17
-27
@@ -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,24 +71,24 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
start_public() {
|
|
||||||
echo -e "=====> 启动公开服务\n"
|
|
||||||
pm2 delete public --source-map-support --time &>/dev/null
|
|
||||||
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
echo -e "=====> 开始检测"
|
echo -e "=====> 开始检测"
|
||||||
npm i -g pnpm
|
npm i -g pnpm@8.3.1
|
||||||
pnpm add -g pm2
|
|
||||||
patch_version
|
patch_version
|
||||||
start_public
|
|
||||||
|
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
+30
-64
@@ -1,16 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
## 导入通用变量与函数
|
|
||||||
dir_shell=$QL_DIR/shell
|
|
||||||
. $dir_shell/share.sh
|
|
||||||
. $dir_shell/api.sh
|
|
||||||
|
|
||||||
trap "single_hanle" 2 20 15 14
|
|
||||||
single_hanle() {
|
|
||||||
handle_task_after "$@"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
random_delay() {
|
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
|
||||||
@@ -35,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 秒后执行\n"
|
echo -e "任务随机延迟 $delay_second 秒,配置文件参数 RandomDelay 置空可取消延迟 \n"
|
||||||
sleep $delay_second
|
sleep $delay_second
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -49,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
|
||||||
@@ -97,43 +86,15 @@ check_server() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_task_before() {
|
env_str_to_array() {
|
||||||
begin_time=$(format_time "$time_format" "$time")
|
local IFS="&"
|
||||||
begin_timestamp=$(format_timestamp "$time_format" "$time")
|
read -ra array <<<"${!env_param}"
|
||||||
|
|
||||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
|
||||||
|
|
||||||
echo -e "## 开始执行... $begin_time\n"
|
|
||||||
|
|
||||||
[[ $is_macos -eq 0 ]] && check_server
|
|
||||||
|
|
||||||
if [[ -s $task_error_log_path ]]; then
|
|
||||||
eval cat $task_error_log_path $cmd
|
|
||||||
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
|
|
||||||
eval echo $cmd
|
|
||||||
fi
|
|
||||||
|
|
||||||
. $file_task_before "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
handle_task_after() {
|
|
||||||
. $file_task_after "$@"
|
|
||||||
|
|
||||||
local etime=$(date "+$time_format")
|
|
||||||
local end_time=$(format_time "$time_format" "$etime")
|
|
||||||
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
|
||||||
local diff_time=$(($end_timestamp - $begin_timestamp))
|
|
||||||
|
|
||||||
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒"
|
|
||||||
echo -e "\n "
|
|
||||||
|
|
||||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## 正常运行单个脚本,$1:传入参数
|
## 正常运行单个脚本,$1:传入参数
|
||||||
run_normal() {
|
run_normal() {
|
||||||
local file_param=$1
|
local file_param=$1
|
||||||
if [[ $# -eq 1 ]]; then
|
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]] && [[ "$no_delay" != "true" ]]; then
|
||||||
random_delay "$file_param"
|
random_delay "$file_param"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -144,7 +105,7 @@ run_normal() {
|
|||||||
file_param=${file_param/$relative_path\//}
|
file_param=${file_param/$relative_path\//}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
$timeoutCmd $which_program $file_param
|
$timeoutCmd $which_program $file_param "${script_params[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||||
@@ -157,8 +118,7 @@ run_concurrent() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local envs=$(eval echo "\$${env_param}")
|
env_str_to_array
|
||||||
local array=($(echo $envs | sed 's/&/ /g'))
|
|
||||||
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||||
local runArr=($(eval echo $tempArr))
|
local runArr=($(eval echo $tempArr))
|
||||||
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
|
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
|
||||||
@@ -169,12 +129,14 @@ run_concurrent() {
|
|||||||
let n++
|
let n++
|
||||||
done
|
done
|
||||||
|
|
||||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
local cookieStr=$(
|
||||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
IFS="&"
|
||||||
|
echo "${array_run[*]}"
|
||||||
|
)
|
||||||
|
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
|
||||||
|
|
||||||
local envs=$(eval echo "\$${env_param}")
|
env_str_to_array
|
||||||
local array=($(echo $envs | sed 's/&/ /g'))
|
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
|
||||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
|
||||||
|
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
local relative_path="${file_param%/*}"
|
local relative_path="${file_param%/*}"
|
||||||
@@ -183,9 +145,9 @@ run_concurrent() {
|
|||||||
file_param=${file_param/$relative_path\//}
|
file_param=${file_param/$relative_path\//}
|
||||||
fi
|
fi
|
||||||
for i in "${!array[@]}"; do
|
for i in "${!array[@]}"; do
|
||||||
export ${env_param}=${array[i]}
|
export "${env_param}=${array[i]}"
|
||||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||||
eval $timeoutCmd $which_program $file_param &>$single_log_path &
|
eval $timeoutCmd $which_program $file_param "${script_params[@]}" &>$single_log_path &
|
||||||
done
|
done
|
||||||
|
|
||||||
wait
|
wait
|
||||||
@@ -205,8 +167,7 @@ run_designated() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local envs=$(eval echo "\$${env_param}")
|
env_str_to_array
|
||||||
local array=($(echo $envs | sed 's/&/ /g'))
|
|
||||||
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||||
local runArr=($(eval echo $tempArr))
|
local runArr=($(eval echo $tempArr))
|
||||||
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
|
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
|
||||||
@@ -217,8 +178,11 @@ run_designated() {
|
|||||||
let n++
|
let n++
|
||||||
done
|
done
|
||||||
|
|
||||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
local cookieStr=$(
|
||||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
IFS="&"
|
||||||
|
echo "${array_run[*]}"
|
||||||
|
)
|
||||||
|
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
|
||||||
|
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
local relative_path="${file_param%/*}"
|
local relative_path="${file_param%/*}"
|
||||||
@@ -226,7 +190,7 @@ run_designated() {
|
|||||||
cd ${relative_path}
|
cd ${relative_path}
|
||||||
file_param=${file_param/$relative_path\//}
|
file_param=${file_param/$relative_path\//}
|
||||||
fi
|
fi
|
||||||
$timeoutCmd $which_program $file_param
|
$timeoutCmd $which_program $file_param "${script_params[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 运行其他命令
|
## 运行其他命令
|
||||||
@@ -237,7 +201,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
|
||||||
@@ -277,6 +241,8 @@ main() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_task_before "$@"
|
handle_task_start "${task_shell_params[@]}"
|
||||||
main "$@"
|
run_task_before "${task_shell_params[@]}"
|
||||||
handle_task_after "$@"
|
main "${task_shell_params[@]}"
|
||||||
|
run_task_after "${task_shell_params[@]}"
|
||||||
|
handle_task_end "${task_shell_params[@]}"
|
||||||
|
|||||||
Regular → Executable
+1
-1
@@ -11,7 +11,7 @@ echo -e "提交master代码"
|
|||||||
git push
|
git push
|
||||||
|
|
||||||
echo -e "更新cdn文件"
|
echo -e "更新cdn文件"
|
||||||
ts-node sample/tool.ts
|
tsx sample/tool.ts
|
||||||
|
|
||||||
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
|
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
|
||||||
version="v$string"
|
version="v$string"
|
||||||
|
|||||||
+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
|
||||||
}
|
}
|
||||||
|
|||||||
+97
-109
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
dir_root=$QL_DIR
|
dir_root=$QL_DIR
|
||||||
|
dir_tmp=$dir_root/.tmp
|
||||||
dir_data=$dir_root/data
|
dir_data=$dir_root/data
|
||||||
dir_shell=$dir_root/shell
|
dir_shell=$dir_root/shell
|
||||||
dir_sample=$dir_root/sample
|
dir_sample=$dir_root/sample
|
||||||
@@ -18,6 +19,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 +36,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 +68,9 @@ 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"}
|
ql_port=${QlPort:-"5700"}
|
||||||
|
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}
|
||||||
@@ -172,6 +174,7 @@ define_cmd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fix_config() {
|
fix_config() {
|
||||||
|
make_dir $dir_tmp
|
||||||
make_dir $dir_static
|
make_dir $dir_static
|
||||||
make_dir $dir_data
|
make_dir $dir_data
|
||||||
make_dir $dir_config
|
make_dir $dir_config
|
||||||
@@ -260,17 +263,7 @@ npm_install_sub() {
|
|||||||
else
|
else
|
||||||
pnpm install --loglevel error --production
|
pnpm install --loglevel error --production
|
||||||
fi
|
fi
|
||||||
}
|
exit_status=$?
|
||||||
|
|
||||||
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() {
|
||||||
@@ -278,12 +271,8 @@ npm_install_2() {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,77 +284,22 @@ diff_and_copy() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
update_depend() {
|
|
||||||
local dir_current=$(pwd)
|
|
||||||
|
|
||||||
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
|
|
||||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
|
||||||
npm_install_2 $dir_scripts
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd $dir_current
|
|
||||||
}
|
|
||||||
|
|
||||||
git_clone_scripts() {
|
git_clone_scripts() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local dir="$2"
|
local dir="$2"
|
||||||
local branch="$3"
|
local branch="$3"
|
||||||
local proxy="$4"
|
local proxy="$4"
|
||||||
[[ $branch ]] && local part_cmd="-b $branch "
|
[[ $branch ]] && local part_cmd="-b $branch "
|
||||||
echo -e "开始克隆仓库 $url 到 $dir\n"
|
echo -e "开始拉取仓库 ${uniq_path} 到 $dir\n"
|
||||||
|
|
||||||
set_proxy "$proxy"
|
set_proxy "$proxy"
|
||||||
git clone $part_cmd $url $dir
|
|
||||||
|
git clone --depth=1 $part_cmd $url $dir 2>&1
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
|
|
||||||
unset_proxy
|
unset_proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
git_pull_scripts() {
|
|
||||||
local dir_current=$(pwd)
|
|
||||||
local dir_work="$1"
|
|
||||||
local branch="$2"
|
|
||||||
local proxy="$3"
|
|
||||||
cd $dir_work
|
|
||||||
echo -e "开始更新仓库:$dir_work\n"
|
|
||||||
|
|
||||||
set_proxy "$proxy"
|
|
||||||
git fetch --all
|
|
||||||
exit_status=$?
|
|
||||||
git pull &>/dev/null
|
|
||||||
unset_proxy
|
|
||||||
|
|
||||||
cd $dir_current
|
|
||||||
}
|
|
||||||
|
|
||||||
reset_romote_url() {
|
|
||||||
local dir_current=$(pwd)
|
|
||||||
local dir_work=$1
|
|
||||||
local url=$2
|
|
||||||
local branch="$3"
|
|
||||||
|
|
||||||
cd $dir_work
|
|
||||||
if [[ -d "$dir_work/.git" ]]; then
|
|
||||||
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
|
||||||
git remote set-url origin $url &>/dev/null
|
|
||||||
else
|
|
||||||
git init
|
|
||||||
git remote add origin $url &>/dev/null
|
|
||||||
fi
|
|
||||||
reset_branch "$branch"
|
|
||||||
cd $dir_current
|
|
||||||
}
|
|
||||||
|
|
||||||
reset_branch() {
|
|
||||||
local branch="$1"
|
|
||||||
local part_cmd=""
|
|
||||||
if [[ $branch ]]; then
|
|
||||||
part_cmd="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
|
|
||||||
}
|
|
||||||
|
|
||||||
random_range() {
|
random_range() {
|
||||||
local beg=$1
|
local beg=$1
|
||||||
local end=$2
|
local end=$2
|
||||||
@@ -373,15 +307,9 @@ random_range() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reload_pm2() {
|
reload_pm2() {
|
||||||
pm2 l &>/dev/null
|
cd $dir_root
|
||||||
|
pm2 flush &>/dev/null
|
||||||
echo -e "启动面板服务\n"
|
env ALL_PROXY= HTTP_PROXY= HTTPS_PROXY= all_proxy= http_proxy= https_proxy= pm2 startOrGracefulReload $file_ecosystem_js --update-env
|
||||||
pm2 delete panel --source-map-support --time &>/dev/null
|
|
||||||
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
|
|
||||||
|
|
||||||
echo -e "启动定时任务服务\n"
|
|
||||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
|
||||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
diff_time() {
|
diff_time() {
|
||||||
@@ -413,9 +341,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,17 +359,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"
|
||||||
@@ -450,14 +370,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
|
||||||
@@ -483,10 +395,86 @@ 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
|
||||||
|
if [[ $ql_base_url != /* ]]; then
|
||||||
|
ql_base_url="/$ql_base_url"
|
||||||
|
fi
|
||||||
|
if [[ $ql_base_url != */ ]]; then
|
||||||
|
ql_base_url="$ql_base_url/"
|
||||||
|
fi
|
||||||
|
location_url="^~${ql_base_url%*/}"
|
||||||
|
aliasStr="alias ${dir_static}/dist;"
|
||||||
|
if ! grep -q "<base href=\"$ql_base_url\">" "${dir_static}/dist/index.html"; then
|
||||||
|
awk -v text="<base href=\"$ql_base_url\">" '/<link/ && !inserted {print text; inserted=1} 1' "${dir_static}/dist/index.html" >temp.html
|
||||||
|
mv temp.html "${dir_static}/dist/index.html"
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
|
||||||
|
local ipv6=$(ip a | grep inet6)
|
||||||
|
local ipv6Str=""
|
||||||
|
if [[ $ipv6 ]]; then
|
||||||
|
ipv6Str="listen [::]:${ql_port} ipv6only=on;"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ipv4Str="listen ${ql_port};"
|
||||||
|
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
|
||||||
|
sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_task_start() {
|
||||||
|
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
echo -e "## 开始执行... $begin_time\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_task_before() {
|
||||||
|
[[ $is_macos -eq 0 ]] && check_server
|
||||||
|
|
||||||
|
. $file_task_before "$@"
|
||||||
|
|
||||||
|
if [[ $task_before ]]; then
|
||||||
|
echo -e "执行前置命令\n"
|
||||||
|
eval "$task_before"
|
||||||
|
echo -e "\n执行前置命令结束\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_task_after() {
|
||||||
|
. $file_task_after "$@"
|
||||||
|
|
||||||
|
if [[ $task_after ]]; then
|
||||||
|
echo -e "\n执行后置命令\n"
|
||||||
|
eval "$task_after"
|
||||||
|
echo -e "\n执行后置命令结束"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_task_end() {
|
||||||
|
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## 执行结束... $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
|
||||||
|
|||||||
+72
-17
@@ -5,17 +5,27 @@ 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 19 1
|
||||||
|
single_hanle() {
|
||||||
|
eval handle_task_end "$@" "$cmd"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
## 选择python3还是node
|
## 选择python3还是node
|
||||||
define_program() {
|
define_program() {
|
||||||
local file_param=$1
|
local file_param=$1
|
||||||
if [[ $file_param == *.js ]]; then
|
if [[ $file_param == *.js ]] || [[ $file_param == *.mjs ]]; then
|
||||||
which_program="node"
|
which_program="node"
|
||||||
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
|
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
|
||||||
which_program="python3"
|
which_program="python3"
|
||||||
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
|
||||||
which_program="ts-node-transpile-only"
|
if ! type tsx &>/dev/null; then
|
||||||
|
which_program="ts-node-transpile-only"
|
||||||
|
else
|
||||||
|
which_program="tsx"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
which_program=""
|
which_program=""
|
||||||
fi
|
fi
|
||||||
@@ -24,16 +34,24 @@ define_program() {
|
|||||||
handle_log_path() {
|
handle_log_path() {
|
||||||
local file_param=$1
|
local file_param=$1
|
||||||
|
|
||||||
if [[ -z $file_param ]];then
|
if [[ -z $file_param ]]; then
|
||||||
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
|
||||||
suffix="_${ID}"
|
if [[ "$ID" -gt 0 ]] 2>/dev/null; then
|
||||||
|
suffix="_${ID}"
|
||||||
|
else
|
||||||
|
ID=""
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
time=$(date "+$time_format")
|
|
||||||
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,18 +65,56 @@ 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"
|
||||||
cmd=">> $dir_log/$log_path 2>&1"
|
|
||||||
[[ "$show_log" == "true" ]] && cmd=""
|
if [[ $real_log_path ]]; then
|
||||||
|
log_path="$real_log_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmd="2>&1 | tee -a $dir_log/$log_path"
|
||||||
make_dir "$dir_log/$log_dir"
|
make_dir "$dir_log/$log_dir"
|
||||||
|
if [[ "$no_tee" == "true" ]]; then
|
||||||
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
|
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 --foreground -s 14 -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')
|
||||||
|
|
||||||
|
# 分割 task 内置参数和脚本参数
|
||||||
|
task_shell_params=()
|
||||||
|
script_params=()
|
||||||
|
found_double_dash=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
if $found_double_dash; then
|
||||||
|
script_params+=("$arg")
|
||||||
|
elif [ "$arg" == "--" ]; then
|
||||||
|
found_double_dash=true
|
||||||
|
else
|
||||||
|
task_shell_params+=("$arg")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
init_begin_time() {
|
||||||
|
begin_time=$(format_time "$time_format" "$time")
|
||||||
|
begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||||
}
|
}
|
||||||
|
|
||||||
while getopts ":lm:" opt; do
|
while getopts ":lm:" opt; do
|
||||||
@@ -78,10 +134,9 @@ if [[ $max_time ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
format_params "$@"
|
format_params "$@"
|
||||||
define_program "$@"
|
define_program "${task_shell_params[@]}"
|
||||||
handle_log_path "$@"
|
handle_log_path "${task_shell_params[@]}"
|
||||||
|
init_begin_time
|
||||||
eval . $dir_shell/otask.sh "$params" "$cmd"
|
|
||||||
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
|
|
||||||
|
|
||||||
|
eval . $dir_shell/otask.sh "$cmd"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+162
-106
@@ -31,7 +31,7 @@ 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}的定时任务:"
|
||||||
cat $list
|
cat $list
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
@@ -44,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
|
||||||
@@ -54,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"
|
||||||
@@ -75,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
|
||||||
@@ -92,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"
|
||||||
@@ -118,6 +118,8 @@ update_repo() {
|
|||||||
local branch="$5"
|
local branch="$5"
|
||||||
local extensions="$6"
|
local extensions="$6"
|
||||||
local proxy="$7"
|
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##*:}"
|
||||||
@@ -128,58 +130,71 @@ update_repo() {
|
|||||||
make_dir "${dir_scripts}/${uniq_path}"
|
make_dir "${dir_scripts}/${uniq_path}"
|
||||||
|
|
||||||
local formatUrl="$url"
|
local formatUrl="$url"
|
||||||
if [[ -d ${repo_path}/.git ]]; then
|
rm -rf ${repo_path} &>/dev/null
|
||||||
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
|
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
|
||||||
git_pull_scripts ${repo_path} "${branch}" "${proxy}"
|
|
||||||
else
|
|
||||||
rm -rf ${repo_path} &>/dev/null
|
|
||||||
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
|
|
||||||
fi
|
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新${repo_path}成功...\n"
|
echo -e "\n拉取 ${uniq_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拉取 ${uniq_path} 失败,请检查网络...\n"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新所有 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}
|
||||||
cron_line=$(
|
if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then
|
||||||
perl -ne "{
|
cron_line=$(
|
||||||
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
|
perl -ne "{
|
||||||
}" $raw_file_name |
|
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
|
||||||
perl -pe "{
|
}" $raw_file_name |
|
||||||
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
|
perl -pe "{
|
||||||
s|\*([\d\*])(.*)|\1\2|g;
|
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
|
||||||
s| | |g;
|
s|\*([\d\*])(.*)|\1\2|g;
|
||||||
}" | sort -u | head -1
|
s| | |g;
|
||||||
)
|
}" | sort -u | head -1
|
||||||
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
|
)
|
||||||
[[ -z $cron_name ]] && cron_name="$raw_file_name"
|
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_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
[[ -z $cron_name ]] && cron_name="$raw_file_name"
|
||||||
[[ -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 ":" '{print $2}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line="$default_cron"
|
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||||
if [[ -z $cron_id ]]; then
|
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||||
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
|
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name:$SUB_ID")
|
||||||
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"
|
||||||
@@ -193,13 +208,10 @@ 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
|
. $file_extra_shell
|
||||||
echo -e "--------------------------------------------------------------\n"
|
else
|
||||||
. $file_extra_shell
|
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
|
||||||
else
|
|
||||||
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,65 +229,99 @@ usage() {
|
|||||||
echo -e "9. $cmd_update resettfa # 禁用两步登录"
|
echo -e "9. $cmd_update resettfa # 禁用两步登录"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reload_qinglong() {
|
||||||
|
local reload_target="${1}"
|
||||||
|
local primary_branch="master"
|
||||||
|
if [[ "${QL_BRANCH}" == "develop" ]]; then
|
||||||
|
primary_branch="develop"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$reload_target" == 'system' ]]; then
|
||||||
|
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
|
||||||
|
rm -rf $dir_static/*
|
||||||
|
cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
|
||||||
|
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$reload_target" == 'data' ]]; then
|
||||||
|
cp -rf ${dir_tmp}/data ${dir_root}/
|
||||||
|
fi
|
||||||
|
|
||||||
|
reload_pm2
|
||||||
|
}
|
||||||
|
|
||||||
## 更新qinglong
|
## 更新qinglong
|
||||||
update_qinglong() {
|
update_qinglong() {
|
||||||
local mirror="github"
|
rm -rf ${dir_tmp}/*
|
||||||
local githubStatus=$(curl -s -m 2 -IL "https://github.com" | grep 200)
|
local mirror="gitee"
|
||||||
if [ "$githubStatus" == "" ]; then
|
local downloadQLUrl="https://gitee.com/whyour/qinglong/repository/archive"
|
||||||
mirror="gitee"
|
local downloadStaticUrl="https://gitee.com/whyour/qinglong-static/repository/archive"
|
||||||
|
local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
|
||||||
|
if [[ ! -z $githubStatus ]]; then
|
||||||
|
mirror="github"
|
||||||
|
downloadQLUrl="https://github.com/whyour/qinglong/archive/refs/heads"
|
||||||
|
downloadStaticUrl="https://github.com/whyour/qinglong-static/archive/refs/heads"
|
||||||
fi
|
fi
|
||||||
echo -e "使用 ${mirror} 源更新...\n"
|
echo -e "使用 ${mirror} 源更新...\n"
|
||||||
export isFirstStartServer=false
|
|
||||||
|
|
||||||
local all_branch=$(cd ${dir_root} && 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)
|
|
||||||
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
|
wget -cqO "${dir_tmp}/ql.zip" "${downloadQLUrl}/${primary_branch}.zip"
|
||||||
git_pull_scripts $dir_root ${primary_branch}
|
exit_status=$?
|
||||||
|
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新青龙源文件成功...\n"
|
echo -e "更新青龙源文件成功...\n"
|
||||||
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
|
|
||||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
|
||||||
update_depend
|
|
||||||
|
|
||||||
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}
|
||||||
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
|
||||||
|
|
||||||
update_qinglong_static "$1" "$primary_branch"
|
update_qinglong_static
|
||||||
else
|
else
|
||||||
echo -e "\n更新青龙源文件失败,请检查网络...\n"
|
echo -e "更新青龙源文件失败,请检查网络...\n"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
update_qinglong_static() {
|
update_qinglong_static() {
|
||||||
local no_restart="$1"
|
wget -cqO "${dir_tmp}/static.zip" "${downloadStaticUrl}/${primary_branch}.zip"
|
||||||
local primary_branch="$2"
|
exit_status=$?
|
||||||
local url="https://${mirror}.com/whyour/qinglong-static.git"
|
|
||||||
if [[ -d ${ql_static_repo}/.git ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
|
echo -e "更新青龙静态资源成功...\n"
|
||||||
git_pull_scripts ${ql_static_repo} ${primary_branch}
|
unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}
|
||||||
|
|
||||||
|
check_update_dep
|
||||||
else
|
else
|
||||||
rm -rf ${ql_static_repo} &>/dev/null
|
echo -e "更新青龙静态资源失败,请检查网络...\n"
|
||||||
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_update_dep() {
|
||||||
|
echo -e "\n开始检测依赖...\n"
|
||||||
|
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
|
||||||
|
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||||
|
npm_install_2 $dir_scripts
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
|
||||||
|
npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新青龙静态资源成功...\n"
|
echo -e "\n依赖检测安装成功...\n"
|
||||||
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
|
echo -e "更新包下载成功..."
|
||||||
|
|
||||||
|
if [[ "$needRestart" == 'true' ]]; then
|
||||||
|
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
|
||||||
|
rm -rf $dir_static/*
|
||||||
|
cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
|
||||||
|
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||||
|
|
||||||
rm -rf $dir_static/*
|
|
||||||
cp -rf $ql_static_repo/* $dir_static
|
|
||||||
if [[ $no_restart != "no-restart" ]]; then
|
|
||||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
|
||||||
echo -e "重启面板中..."
|
|
||||||
sleep 3
|
|
||||||
reload_pm2
|
reload_pm2
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "\n更新青龙静态资源失败,请检查网络...\n"
|
echo -e "\n依赖检测安装失败,请检查网络...\n"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +334,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"
|
||||||
|
|
||||||
@@ -297,13 +352,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
|
||||||
@@ -341,10 +396,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}"
|
||||||
@@ -352,7 +407,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}"
|
||||||
@@ -365,7 +420,7 @@ 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}')
|
||||||
@@ -404,13 +459,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")
|
||||||
@@ -431,12 +489,6 @@ main() {
|
|||||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -s $task_error_log_path ]]; then
|
|
||||||
eval cat $task_error_log_path $cmd
|
|
||||||
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
|
|
||||||
eval echo $cmd
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
|
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
|
||||||
eval echo -e "请移除 -l 参数" $cmd
|
eval echo -e "请移除 -l 参数" $cmd
|
||||||
exit 1
|
exit 1
|
||||||
@@ -444,7 +496,12 @@ main() {
|
|||||||
|
|
||||||
case $p1 in
|
case $p1 in
|
||||||
update)
|
update)
|
||||||
eval update_qinglong "$2" $cmd
|
fix_config
|
||||||
|
local needRestart=${p2:-"true"}
|
||||||
|
eval update_qinglong $cmd
|
||||||
|
;;
|
||||||
|
reload)
|
||||||
|
eval reload_qinglong "$p2" $cmd
|
||||||
;;
|
;;
|
||||||
extra)
|
extra)
|
||||||
eval run_extra_shell $cmd
|
eval run_extra_shell $cmd
|
||||||
@@ -452,7 +509,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" "$p8"
|
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
|
||||||
@@ -461,7 +518,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
|
||||||
@@ -499,8 +556,7 @@ main() {
|
|||||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
|
||||||
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
|
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
|
||||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒 " $cmd
|
||||||
eval echo -e "\\\n " $cmd
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f $file_path ]]; then
|
if [[ -f $file_path ]]; then
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
const baseUrl = window.__ENV__QlBaseUrl || '/';
|
||||||
|
import { setLocale } from '@umijs/max';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
export function rootContainer(container: any) {
|
||||||
|
const locales = {
|
||||||
|
'en': require('./locales/en-US.json'),
|
||||||
|
'zh': require('./locales/zh-CN.json'),
|
||||||
|
};
|
||||||
|
let currentLocale = intl.determineLocale({
|
||||||
|
urlLocaleKey: 'lang',
|
||||||
|
cookieLocaleKey: 'lang',
|
||||||
|
localStorageLocaleKey: 'lang',
|
||||||
|
}).slice(0, 2);
|
||||||
|
|
||||||
|
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
|
||||||
|
currentLocale = 'zh';
|
||||||
|
}
|
||||||
|
|
||||||
|
intl.init({ currentLocale, locales });
|
||||||
|
setLocale(currentLocale === 'zh' ? 'zh-CN' : 'en-US');
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modifyClientRenderOpts(memo: any) {
|
||||||
|
return {
|
||||||
|
...memo,
|
||||||
|
publicPath: baseUrl,
|
||||||
|
basename: baseUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modifyContextOpts(memo: any) {
|
||||||
|
return {
|
||||||
|
...memo,
|
||||||
|
basename: baseUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import intl from 'react-intl-universal';
|
||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import { Tooltip, Typography } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
|
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
|
||||||
@@ -28,7 +29,10 @@ const Copy = ({ text }: { text: string }) => {
|
|||||||
return (
|
return (
|
||||||
<Link onClick={copyText} style={{ marginLeft: 1 }}>
|
<Link onClick={copyText} style={{ marginLeft: 1 }}>
|
||||||
<CopyToClipboard text={text}>
|
<CopyToClipboard text={text}>
|
||||||
<Tooltip key="copy" title={copied ? '复制成功' : '复制'}>
|
<Tooltip
|
||||||
|
key="copy"
|
||||||
|
title={copied ? intl.get('复制成功') : intl.get('复制')}
|
||||||
|
>
|
||||||
{copied ? <CheckOutlined /> : <CopyOutlined />}
|
{copied ? <CheckOutlined /> : <CopyOutlined />}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useRequest } from 'ahooks';
|
||||||
|
import { Service, Options } from 'ahooks/lib/useRequest/src/types';
|
||||||
|
import { Spin, Typography } from 'antd';
|
||||||
|
|
||||||
|
export default function Name<
|
||||||
|
TData extends { data?: { name: string } },
|
||||||
|
TParams,
|
||||||
|
>({
|
||||||
|
service,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
service: Service<TData, [TParams]>;
|
||||||
|
options: Options<TData, [TParams]>;
|
||||||
|
}) {
|
||||||
|
const { loading, data } = useRequest(service, options);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Typography.Text ellipsis={true}>{data?.data?.name}</Typography.Text>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import intl from 'react-intl-universal';
|
||||||
import { Tag, Input } from 'antd';
|
import { Tag, Input } from 'antd';
|
||||||
import { TweenOneGroup } from 'rc-tween-one';
|
import { TweenOneGroup } from 'rc-tween-one';
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
@@ -101,7 +102,7 @@ const EditableTagGroup = ({
|
|||||||
onClick={showInput}
|
onClick={showInput}
|
||||||
style={{ borderStyle: 'dashed', cursor: 'pointer' }}
|
style={{ borderStyle: 'dashed', cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> 新建
|
<PlusOutlined /> {intl.get('新建')}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { RefObject, useState } from 'react';
|
||||||
|
import useResizeObserver from '@react-hook/resize-observer';
|
||||||
|
|
||||||
|
export default <T extends HTMLElement>(target: RefObject<T>) => {
|
||||||
|
const [height, setHeight] = useState<number>(0);
|
||||||
|
|
||||||
|
useResizeObserver(target, (entry) => {
|
||||||
|
let _height = entry.target.clientHeight;
|
||||||
|
if (height !== _height) {
|
||||||
|
setHeight(_height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return height;
|
||||||
|
};
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import { MutableRefObject, useLayoutEffect, useState } from 'react';
|
import { RefObject, useState } from 'react';
|
||||||
import useResizeObserver from '@react-hook/resize-observer'
|
import useResizeObserver from '@react-hook/resize-observer';
|
||||||
import { getTableScroll } from '@/utils';
|
import { getTableScroll } from '@/utils';
|
||||||
|
|
||||||
export default <T extends HTMLElement>(target: MutableRefObject<T>, extraHeight?: number) => {
|
export default <T extends HTMLElement>(
|
||||||
const [height, setHeight] = useState<number>()
|
target: RefObject<T>,
|
||||||
|
extraHeight?: number,
|
||||||
|
) => {
|
||||||
|
const [height, setHeight] = useState<number>(0);
|
||||||
|
|
||||||
useResizeObserver(target, (entry) => {
|
useResizeObserver(target, (entry) => {
|
||||||
let _targe = entry.target as any
|
let _target = entry.target as any;
|
||||||
if (!_targe.classList.contains('ant-table-wrapper')) {
|
if (!_target.classList.contains('ant-table-wrapper')) {
|
||||||
_targe = entry.target.querySelector('.ant-table-wrapper')
|
_target = entry.target.querySelector('.ant-table-wrapper');
|
||||||
}
|
}
|
||||||
setHeight(getTableScroll({ extraHeight, target: _targe as HTMLElement }))
|
setHeight(getTableScroll({ extraHeight, target: _target as HTMLElement }));
|
||||||
})
|
});
|
||||||
return height
|
return height;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import {
|
import intl from 'react-intl-universal';
|
||||||
FormOutlined,
|
import { SettingOutlined } from '@ant-design/icons';
|
||||||
FieldTimeOutlined,
|
|
||||||
DiffOutlined,
|
|
||||||
SettingOutlined,
|
|
||||||
CodeOutlined,
|
|
||||||
FolderOutlined,
|
|
||||||
RadiusSettingOutlined,
|
|
||||||
ControlOutlined,
|
|
||||||
ContainerOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import IconFont from '@/components/iconfont';
|
import IconFont from '@/components/iconfont';
|
||||||
import { BasicLayoutProps } from '@ant-design/pro-layout';
|
import { BasicLayoutProps } from '@ant-design/pro-layout';
|
||||||
|
|
||||||
@@ -16,74 +7,74 @@ export default {
|
|||||||
route: {
|
route: {
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
name: '登录',
|
name: intl.get('登录'),
|
||||||
path: '/login',
|
path: '/login',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: '@/pages/login/index',
|
component: '@/pages/login/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '初始化',
|
name: intl.get('初始化'),
|
||||||
path: '/initialization',
|
path: '/initialization',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: '@/pages/initialization/index',
|
component: '@/pages/initialization/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '错误',
|
name: intl.get('错误'),
|
||||||
path: '/error',
|
path: '/error',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: '@/pages/error/index',
|
component: '@/pages/error/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/crontab',
|
path: '/crontab',
|
||||||
name: '定时任务',
|
name: intl.get('定时任务'),
|
||||||
icon: <IconFont type="ql-icon-crontab" />,
|
icon: <IconFont type="ql-icon-crontab" />,
|
||||||
component: '@/pages/crontab/index',
|
component: '@/pages/crontab/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/subscription',
|
path: '/subscription',
|
||||||
name: '订阅管理',
|
name: intl.get('订阅管理'),
|
||||||
icon: <IconFont type="ql-icon-subs" />,
|
icon: <IconFont type="ql-icon-subs" />,
|
||||||
component: '@/pages/subscription/index',
|
component: '@/pages/subscription/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/env',
|
path: '/env',
|
||||||
name: '环境变量',
|
name: intl.get('环境变量'),
|
||||||
icon: <IconFont type="ql-icon-env" />,
|
icon: <IconFont type="ql-icon-env" />,
|
||||||
component: '@/pages/env/index',
|
component: '@/pages/env/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/config',
|
path: '/config',
|
||||||
name: '配置文件',
|
name: intl.get('配置文件'),
|
||||||
icon: <IconFont type="ql-icon-config" />,
|
icon: <IconFont type="ql-icon-config" />,
|
||||||
component: '@/pages/config/index',
|
component: '@/pages/config/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/script',
|
path: '/script',
|
||||||
name: '脚本管理',
|
name: intl.get('脚本管理'),
|
||||||
icon: <IconFont type="ql-icon-script" />,
|
icon: <IconFont type="ql-icon-script" />,
|
||||||
component: '@/pages/script/index',
|
component: '@/pages/script/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/dependence',
|
path: '/dependence',
|
||||||
name: '依赖管理',
|
name: intl.get('依赖管理'),
|
||||||
icon: <IconFont type="ql-icon-dependence" />,
|
icon: <IconFont type="ql-icon-dependence" />,
|
||||||
component: '@/pages/dependence/index',
|
component: '@/pages/dependence/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/log',
|
path: '/log',
|
||||||
name: '日志管理',
|
name: intl.get('日志管理'),
|
||||||
icon: <IconFont type="ql-icon-log" />,
|
icon: <IconFont type="ql-icon-log" />,
|
||||||
component: '@/pages/log/index',
|
component: '@/pages/log/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/diff',
|
path: '/diff',
|
||||||
name: '对比工具',
|
name: intl.get('对比工具'),
|
||||||
icon: <IconFont type="ql-icon-diff" />,
|
icon: <IconFont type="ql-icon-diff" />,
|
||||||
component: '@/pages/diff/index',
|
component: '@/pages/diff/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/setting',
|
path: '/setting',
|
||||||
name: '系统设置',
|
name: intl.get('系统设置'),
|
||||||
icon: <SettingOutlined />,
|
icon: <SettingOutlined />,
|
||||||
component: '@/pages/password/index',
|
component: '@/pages/password/index',
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user