mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4337008eb | ||
|
|
ca5cb88eca | ||
|
|
5afac3a3ac | ||
|
|
71ba1534f2 | ||
|
|
791cf657b5 | ||
|
|
c1ef021009 | ||
|
|
90fe63211d | ||
|
|
b60cda66bb | ||
|
|
7efe81df9e | ||
|
|
0d492e94f4 | ||
|
|
a45efbd69b | ||
|
|
035f0eb9e3 | ||
|
|
7d0cae7839 | ||
|
|
46e71d8213 | ||
|
|
372afb92c6 | ||
|
|
69d9307be9 | ||
|
|
c3908e956f | ||
|
|
1e749f3731 | ||
|
|
a68f4b0cde | ||
|
|
07541569c5 | ||
|
|
70f2bef5d2 | ||
|
|
805a716033 | ||
|
|
e77d4c4337 | ||
|
|
b309ac7613 | ||
|
|
8dd379b6b9 | ||
|
|
0b52e05af8 | ||
|
|
e9852a0002 | ||
|
|
20be84b352 | ||
|
|
4ab24ea1b9 | ||
|
|
417f91207f | ||
|
|
f723631647 | ||
|
|
6657ff0560 | ||
|
|
0593dae41d | ||
|
|
445dee00f7 | ||
|
|
6ca28190b0 | ||
|
|
510534ee0c | ||
|
|
2e7f3a1578 | ||
|
|
591a17e7ee | ||
|
|
bd7c361300 | ||
|
|
409b3281cc | ||
|
|
ac6de4911a | ||
|
|
1628e05ece | ||
|
|
7b7e03b503 | ||
|
|
8d14c9dae1 | ||
|
|
4d6d0a55e7 | ||
|
|
8ab2dc3280 | ||
|
|
3f54048127 | ||
|
|
47c2c61f33 | ||
|
|
47d2fc24bc | ||
|
|
7a8a8ab9b3 | ||
|
|
2ac4581d54 | ||
|
|
68ad01e0e8 | ||
|
|
cdeca4b808 | ||
|
|
55945e4cd1 | ||
|
|
b39036f8f8 | ||
|
|
f07093d29f | ||
|
|
11c789c71c | ||
|
|
81898f9dd7 | ||
|
|
6dba8ae72d | ||
|
|
c47896e787 | ||
|
|
14cb1f7788 | ||
|
|
892d91edc4 | ||
|
|
74916670eb | ||
|
|
b31eab02d7 | ||
|
|
9c258ab113 | ||
|
|
3bcba11b33 | ||
|
|
15140fd87d | ||
|
|
e83058c3bc | ||
|
|
29f0c2c9ac | ||
|
|
f25fe27473 | ||
|
|
0ae1f284ec | ||
|
|
49c441117a | ||
|
|
7bc5ff01a6 | ||
|
|
8128fb8a9f | ||
|
|
315ae49360 | ||
|
|
35c6475534 | ||
|
|
7f3e60423b | ||
|
|
0a723318a1 | ||
|
|
2c1e7535b8 | ||
|
|
48d1fba699 | ||
|
|
06f861baaf | ||
|
|
bbbd353567 | ||
|
|
8283f62db2 | ||
|
|
1a94a660a5 | ||
|
|
5c393fef36 | ||
|
|
53346a3962 | ||
|
|
4e66a3aff5 | ||
|
|
b79d10b452 | ||
|
|
3dc3ece993 | ||
|
|
792cf9ad20 | ||
|
|
201f41e15b | ||
|
|
71e7ba9cb6 | ||
|
|
a7562259d1 | ||
|
|
18671264bf | ||
|
|
37bcb77be0 | ||
|
|
563457cf1b | ||
|
|
d2cf482d11 | ||
|
|
2713942a68 | ||
|
|
62168979e3 | ||
|
|
f8aba4b1fb | ||
|
|
52a89a21f6 | ||
|
|
ca8790bbec | ||
|
|
be1d68c0d1 | ||
|
|
95afcbc9dd | ||
|
|
20249119a0 | ||
|
|
6e0ee6f627 | ||
|
|
13e7dac333 | ||
|
|
b89dc6dfc6 | ||
|
|
897b710527 | ||
|
|
5afec03548 | ||
|
|
c6a25e5359 | ||
|
|
3777a4e7b4 | ||
|
|
9b7d4cb8fe | ||
|
|
59f7b72bb1 | ||
|
|
4d97f0f44d | ||
|
|
de9e9ce627 | ||
|
|
f056b611a4 | ||
|
|
659484511f | ||
|
|
9ed107980c | ||
|
|
4a3ac7dc4b | ||
|
|
c3a0f58003 | ||
|
|
23781d8b34 | ||
|
|
c1d6a0a40b | ||
|
|
82c9978514 | ||
|
|
9f6d4943ea | ||
|
|
c4a44a48c5 | ||
|
|
d2fef75e01 | ||
|
|
085cb789b5 | ||
|
|
20f615eadf | ||
|
|
66a2769e7c | ||
|
|
899a30eacd | ||
|
|
8760ac2964 | ||
|
|
62a29867a3 | ||
|
|
4938635ef4 | ||
|
|
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 |
@@ -1,3 +1,4 @@
|
||||
UPDATE_PORT=5300
|
||||
PUBLIC_PORT=5400
|
||||
CRON_PORT=5500
|
||||
BACK_PORT=5600
|
||||
|
||||
@@ -2,54 +2,52 @@ name: Build And Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
branches:
|
||||
- 'master'
|
||||
- 'develop'
|
||||
- "master"
|
||||
- "develop"
|
||||
- "test-cli"
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
- "v*"
|
||||
schedule:
|
||||
# 参考 https://jasonet.co/posts/scheduled-actions/
|
||||
- cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
|
||||
- cron: "00 20 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
to_gitlab:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
with:
|
||||
target_repo_url:
|
||||
git@gitlab.com:whyour/qinglong.git
|
||||
ssh_private_key:
|
||||
${{ secrets.GITLAB_SSH_PK }}
|
||||
target_repo_url: git@gitlab.com:whyour/qinglong.git
|
||||
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||
|
||||
to_gitee:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
with:
|
||||
target_repo_url:
|
||||
git@gitee.com:whyour/qinglong.git
|
||||
ssh_private_key:
|
||||
${{ secrets.GITLAB_SSH_PK }}
|
||||
target_repo_url: git@gitee.com:whyour/qinglong.git
|
||||
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||
|
||||
build-static:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: '8.3.1'
|
||||
version: "8.3.1"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
cache: "pnpm"
|
||||
|
||||
- name: build front and back
|
||||
run: |
|
||||
@@ -81,40 +79,43 @@ jobs:
|
||||
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
|
||||
git remote add gitee "${REPO_GITEE}"
|
||||
git remote add gitlab "${REPO_GITLAB}"
|
||||
git gc
|
||||
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||
|
||||
build:
|
||||
needs: build-static
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用
|
||||
runs-on: ubuntu-20.04
|
||||
# runs-on: self-hosted
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: '8.3.1'
|
||||
- uses: actions/setup-node@v3
|
||||
version: "8.3.1"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Setup timezone
|
||||
uses: zcong1993/setup-timezone@master
|
||||
uses: szenius/set-timezone@v1.2
|
||||
with:
|
||||
timezone: Asia/Shanghai
|
||||
timezoneLinux: Asia/Shanghai
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -122,7 +123,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
@@ -142,14 +143,14 @@ jobs:
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
build-args: |
|
||||
MAINTAINER=${{ github.repository_owner }}
|
||||
@@ -157,6 +158,7 @@ jobs:
|
||||
SOURCE_COMMIT=${{ github.sha }}
|
||||
network: host
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||
# platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
push: true
|
||||
@@ -166,4 +168,69 @@ jobs:
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
run: |
|
||||
echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
build310:
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
needs: build-static
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: "8.3.1"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Setup timezone
|
||||
uses: szenius/set-timezone@v1.2
|
||||
with:
|
||||
timezoneLinux: Asia/Shanghai
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push python3.10
|
||||
id: docker_build_310
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
build-args: |
|
||||
MAINTAINER=${{ github.repository_owner }}
|
||||
QL_BRANCH=${{ github.ref_name }}
|
||||
SOURCE_COMMIT=${{ github.sha }}
|
||||
network: host
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||
context: .
|
||||
file: ./docker/310.Dockerfile
|
||||
push: true
|
||||
tags: whyour/qinglong:python3.10
|
||||
cache-from: type=registry,ref=whyour/qinglong:cache-python3.10
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max
|
||||
|
||||
- name: Image digest
|
||||
run: |
|
||||
echo ${{ steps.docker_build_310.outputs.digest }}
|
||||
|
||||
+1
-7
@@ -22,10 +22,4 @@
|
||||
.env
|
||||
.history
|
||||
.version.ts
|
||||
|
||||
/config
|
||||
/log
|
||||
/db
|
||||
/manual_log
|
||||
/scripts
|
||||
/bak
|
||||
/.tmp
|
||||
@@ -1,2 +1 @@
|
||||
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
|
||||
strict-peer-dependencies=false
|
||||
@@ -6,3 +6,20 @@ package.json
|
||||
.umi
|
||||
.umi-production
|
||||
.umi-test
|
||||
.history
|
||||
.tmp
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
static
|
||||
data
|
||||
DS_Store
|
||||
src/.umi
|
||||
src/.umi-production
|
||||
src/.umi-test
|
||||
.env.local
|
||||
.env
|
||||
version.ts
|
||||
/.tmp
|
||||
@@ -4,15 +4,23 @@ const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const baseUrl = process.env.QlBaseUrl || '/';
|
||||
export default defineConfig({
|
||||
hash: true,
|
||||
jsMinifier: 'terser',
|
||||
antd: {},
|
||||
locale: {
|
||||
antd: true,
|
||||
title: true,
|
||||
baseNavigator: true,
|
||||
},
|
||||
outputPath: 'static/dist',
|
||||
fastRefresh: true,
|
||||
favicons: [`https://qn.whyour.cn/favicon.svg`],
|
||||
mfsu: {
|
||||
strategy: 'eager',
|
||||
},
|
||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||
proxy: {
|
||||
[`${baseUrl}api/update`]: {
|
||||
target: 'http://127.0.0.1:5300/',
|
||||
changeOrigin: true,
|
||||
pathRewrite: { [`^${baseUrl}api/update`]: '/api' },
|
||||
},
|
||||
[`${baseUrl}api/public`]: {
|
||||
target: 'http://127.0.0.1:5400/',
|
||||
changeOrigin: true,
|
||||
@@ -44,4 +52,10 @@ export default defineConfig({
|
||||
'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',
|
||||
],
|
||||
copy: [
|
||||
{
|
||||
from: 'node_modules/monaco-editor/min/vs',
|
||||
to: 'static/dist/monaco-editor/min/vs',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
+76
-39
@@ -35,6 +35,25 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
- Support dark mode
|
||||
- Support cell phone operation
|
||||
|
||||
## Version
|
||||
|
||||
### docker
|
||||
|
||||
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
||||
|
||||
```bash
|
||||
docker pull whyour/qinglong:latest
|
||||
docker pull whyour/qinglong:debian
|
||||
```
|
||||
|
||||
### npm
|
||||
|
||||
The npm version supports `debian/ubuntu/alpine` systems and requires `node/npm/python3/pip3/pnpm` to be installed.
|
||||
|
||||
```bash
|
||||
npm i @whyour/qinglong
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker (Recommended)
|
||||
@@ -43,7 +62,12 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
# 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 \
|
||||
@@ -70,17 +94,28 @@ docker-compose down
|
||||
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
|
||||
### Npm
|
||||
|
||||
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
|
||||
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3/pnpm yourself
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
```
|
||||
|
||||
```bash
|
||||
npm install -g node-pre-gyp pnpm@8.3.1
|
||||
npm install -g @whyour/qinglong
|
||||
qinglong
|
||||
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
|
||||
@@ -90,9 +125,28 @@ export QL_DATA_DIR=""
|
||||
qinglong
|
||||
```
|
||||
|
||||
## Use
|
||||
## Built-in commands
|
||||
|
||||
1. built-in commands
|
||||
- task
|
||||
|
||||
```bash
|
||||
# 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>
|
||||
# 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
|
||||
```
|
||||
|
||||
- ql
|
||||
|
||||
```bash
|
||||
# Update and restart Green Dragon
|
||||
@@ -113,50 +167,33 @@ ql check
|
||||
ql resetlet
|
||||
# Disable two-step login
|
||||
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
|
||||
|
||||
* file_url: Script 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
|
||||
* 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
|
||||
* branch: Pull the branch of the repository
|
||||
* days: Number of days of logs to be kept
|
||||
* file_path: File path for task execution
|
||||
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
|
||||
* account_number: Specify the account number of an environment variable to be executed when the task is executed
|
||||
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
|
||||
| **Parameter** | **Description** |
|
||||
|---|---|
|
||||
| file_url | Script 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 |
|
||||
| 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 |
|
||||
| extensions | Pull the branch of the repository |
|
||||
| branch | Number of days of logs to be kept |
|
||||
| days | File path for task execution |
|
||||
| file_path | The name of the environment variable that needs to be concurrent or specified at the time of task execution |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
$ git clone git@github.com:whyour/qinglong.git
|
||||
$ cd qinglong
|
||||
$ cp .env.example .env
|
||||
git clone https://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
|
||||
npm install -g pnpm@8.3.1
|
||||
pnpm install
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Open your browser and visit http://127.0.0.1:5700
|
||||
Open your browser and visit <http://127.0.0.1:5700>
|
||||
|
||||
## Links
|
||||
|
||||
|
||||
@@ -37,6 +37,25 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
- 支持暗黑模式
|
||||
- 支持手机端操作
|
||||
|
||||
## 版本
|
||||
|
||||
### docker
|
||||
|
||||
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
||||
|
||||
```bash
|
||||
docker pull whyour/qinglong:latest
|
||||
docker pull whyour/qinglong:debian
|
||||
```
|
||||
|
||||
### npm
|
||||
|
||||
npm 版本支持 `debian/ubuntu/alpine` 系统,需要自行安装 `node/npm/python3/pip3/pnpm`
|
||||
|
||||
```bash
|
||||
npm i @whyour/qinglong
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
### docker (推荐)
|
||||
@@ -45,9 +64,12 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
# curl -sSL get.docker.com | sh
|
||||
docker run -dit \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
# 部署路径非必须,比如 /test
|
||||
-e QlBaseUrl="/" \
|
||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||
-e QlPort="5700" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
@@ -74,19 +96,28 @@ docker-compose down
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
# 部署路径非必须,比如 /test
|
||||
-e QlBaseUrl="/" \
|
||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||
-e QlPort="5700" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### 本机
|
||||
### npm
|
||||
|
||||
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3
|
||||
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3/pnpm
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
```
|
||||
|
||||
```bash
|
||||
npm install -g node-pre-gyp pnpm@8.3.1
|
||||
npm install -g @whyour/qinglong
|
||||
qinglong
|
||||
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
|
||||
@@ -96,9 +127,26 @@ export QL_DATA_DIR=""
|
||||
qinglong
|
||||
```
|
||||
|
||||
## 使用
|
||||
## 内置命令
|
||||
|
||||
1. 内置命令
|
||||
- task
|
||||
|
||||
```bash
|
||||
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||
task <file_path>
|
||||
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||
task <file_path> now
|
||||
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
||||
task <file_path> conc <env_name> <account_number>(可选的)
|
||||
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
||||
task <file_path> desi <env_name> <account_number>
|
||||
# 设置任务超时时间
|
||||
task -m <max_time> <file_path>
|
||||
# 使用 -- 分割,-- 后面的参数会传给脚本,下面的例子,脚本就可接收到参数 -u whyour -p password
|
||||
task <file_path> -- -u whyour -p password
|
||||
```
|
||||
|
||||
- ql
|
||||
|
||||
```bash
|
||||
# 更新并重启青龙
|
||||
@@ -119,49 +167,33 @@ ql check
|
||||
ql resetlet
|
||||
# 禁用两步登录
|
||||
ql resettfa
|
||||
|
||||
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||
task <file_path>
|
||||
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||
task <file_path> now
|
||||
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
||||
task <file_path> conc <env_name> <account_number>(可选的)
|
||||
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
||||
task <file_path> desi <env_name> <account_number>
|
||||
# 设置任务超时时间
|
||||
task -m <max_time> <file_path>
|
||||
# 实时打印任务日志,创建定时任务时,不用携带此参数
|
||||
task -l <file_path>
|
||||
```
|
||||
|
||||
2. 参数说明
|
||||
|
||||
* file_url: 脚本地址
|
||||
* repo_url: 仓库地址
|
||||
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
|
||||
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
|
||||
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
|
||||
* extensions: 拉取仓库的文件后缀,多个竖线分割
|
||||
* branch: 拉取仓库的分支
|
||||
* days: 需要保留的日志的天数
|
||||
* file_path: 任务执行时的文件路径
|
||||
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||
| **参数** | **说明** |
|
||||
|------------|---------------------------------------------------------------------------------------------|
|
||||
| file_url | 脚本地址 |
|
||||
| repo_url | 仓库地址 |
|
||||
| whitelist | 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割 |
|
||||
| blacklist | 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割 |
|
||||
| dependence | 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割 |
|
||||
| extensions | 拉取仓库的文件后缀,多个竖线分割 |
|
||||
| branch | 拉取仓库的分支 |
|
||||
| days | 需要保留的日志的天数 |
|
||||
| file_path | 任务执行时的文件路径 |
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
$ git clone git@github.com:whyour/qinglong.git
|
||||
$ cd qinglong
|
||||
$ cp .env.example .env
|
||||
git clone https://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
|
||||
npm install -g pnpm@8.3.1
|
||||
pnpm install
|
||||
pnpm start
|
||||
```
|
||||
|
||||
打开你的浏览器,访问 http://127.0.0.1:5700
|
||||
打开你的浏览器,访问 <http://127.0.0.1:5700>
|
||||
|
||||
## 链接
|
||||
|
||||
|
||||
+38
-20
@@ -3,20 +3,36 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import { join } from 'path';
|
||||
import { SAMPLE_FILES } from '../config/const';
|
||||
import ConfigService from '../services/config';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/configs', route);
|
||||
|
||||
route.get(
|
||||
'/sample',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.send({
|
||||
code: 200,
|
||||
data: SAMPLE_FILES,
|
||||
});
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/files',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const fileList = fs.readdirSync(config.configPath, 'utf-8');
|
||||
const fileList = await fs.readdir(config.configPath, 'utf-8');
|
||||
res.send({
|
||||
code: 200,
|
||||
data: fileList
|
||||
@@ -32,24 +48,11 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/:file',
|
||||
'/detail',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let content = '';
|
||||
if (config.blackFileList.includes(req.params.file)) {
|
||||
res.send({ code: 403, message: '文件无法访问' });
|
||||
}
|
||||
if (req.params.file.includes('sample')) {
|
||||
content = getFileContentByName(
|
||||
join(config.samplePath, req.params.file),
|
||||
);
|
||||
} else {
|
||||
content = getFileContentByName(
|
||||
join(config.configPath, req.params.file),
|
||||
);
|
||||
}
|
||||
res.send({ code: 200, data: content });
|
||||
const configService = Container.get(ConfigService);
|
||||
await configService.getFile(req.query.path as string, res);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
@@ -71,12 +74,27 @@ export default (app: Router) => {
|
||||
if (config.blackFileList.includes(name)) {
|
||||
res.send({ code: 403, message: '文件无法访问' });
|
||||
}
|
||||
const path = join(config.configPath, name);
|
||||
fs.writeFileSync(path, content);
|
||||
let path = join(config.configPath, name);
|
||||
if (name.startsWith('data/scripts/')) {
|
||||
path = join(config.rootPath, name);
|
||||
}
|
||||
await fs.writeFile(path, content);
|
||||
res.send({ code: 200, message: '保存成功' });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const configService = Container.get(ConfigService);
|
||||
await configService.getFile(req.params.file, res);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+8
-3
@@ -176,6 +176,9 @@ export default (app: Router) => {
|
||||
name: Joi.string().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) => {
|
||||
@@ -333,6 +336,9 @@ export default (app: Router) => {
|
||||
schedule: Joi.string().required(),
|
||||
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(),
|
||||
}),
|
||||
}),
|
||||
@@ -452,13 +458,12 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.status({
|
||||
...req.body,
|
||||
status: parseInt(req.body.status),
|
||||
pid: parseInt(req.body.pid) || '',
|
||||
status: req.body.status ? parseInt(req.body.status) : undefined,
|
||||
pid: req.body.pid ? parseInt(req.body.pid) : undefined,
|
||||
});
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
|
||||
@@ -134,4 +134,20 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/cancel',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
await dependenceService.cancel(req.body);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+6
-2
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
|
||||
import multer from 'multer';
|
||||
import config from '../config';
|
||||
import fs from 'fs';
|
||||
import { safeJSONParse } from '../config/util';
|
||||
const route = Router();
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
@@ -50,6 +51,9 @@ export default (app: Router) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
if (!req.body?.length) {
|
||||
return res.send({ code: 400, message: '参数不正确' });
|
||||
}
|
||||
const data = await envService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
@@ -200,7 +204,7 @@ export default (app: Router) => {
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
|
||||
const parseContent = JSON.parse(fileContent);
|
||||
const parseContent = safeJSONParse(fileContent);
|
||||
const data = Array.isArray(parseContent)
|
||||
? parseContent
|
||||
: [parseContent];
|
||||
@@ -216,7 +220,7 @@ export default (app: Router) => {
|
||||
} else {
|
||||
return res.send({
|
||||
code: 400,
|
||||
message: '文件缺少name或者value字段,参考导出文件格式',
|
||||
message: '每条数据 name 或者 value 字段不能为空,参考导出文件格式',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
+25
-11
@@ -3,7 +3,7 @@ import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import config from '../config';
|
||||
import { emptyDir, getFileContentByName, readDirs } from '../config/util';
|
||||
import { getFileContentByName, readDirs, rmPath } from '../config/util';
|
||||
import { join } from 'path';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
const route = Router();
|
||||
@@ -15,7 +15,7 @@ export default (app: Router) => {
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const result = readDirs(config.logPath, config.logPath, blacklist);
|
||||
const result = await readDirs(config.logPath, config.logPath, blacklist);
|
||||
res.send({
|
||||
code: 200,
|
||||
data: result,
|
||||
@@ -26,10 +26,29 @@ export default (app: Router) => {
|
||||
}
|
||||
});
|
||||
|
||||
route.get(
|
||||
'/detail',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (blacklist.includes(req.path)) {
|
||||
return res.send({ code: 403, message: '暂无权限' });
|
||||
}
|
||||
const filePath = join(
|
||||
config.logPath,
|
||||
(req.query.path || '') as string,
|
||||
req.query.file as string,
|
||||
);
|
||||
const content = await getFileContentByName(filePath);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
if (blacklist.includes(req.path)) {
|
||||
return res.send({ code: 403, message: '暂无权限' });
|
||||
@@ -39,7 +58,7 @@ export default (app: Router) => {
|
||||
(req.query.path || '') as string,
|
||||
req.params.file,
|
||||
);
|
||||
const content = getFileContentByName(filePath);
|
||||
const content = await getFileContentByName(filePath);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -53,7 +72,7 @@ export default (app: Router) => {
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
type: Joi.string().optional()
|
||||
type: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -64,16 +83,11 @@ export default (app: Router) => {
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.logPath, path, filename);
|
||||
if (type === 'directory') {
|
||||
emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
await rmPath(filePath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
+56
-24
@@ -4,13 +4,13 @@ import {
|
||||
readDirs,
|
||||
getLastModifyFilePath,
|
||||
readDir,
|
||||
emptyDir,
|
||||
rmPath,
|
||||
} from '../config/util';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import path, { join, parse } from 'path';
|
||||
import ScriptService from '../services/script';
|
||||
@@ -34,15 +34,33 @@ export default (app: Router) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let result = [];
|
||||
const blacklist = ['node_modules', '.git'];
|
||||
const blacklist = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'.pnpm',
|
||||
'pnpm-lock.yaml',
|
||||
'yarn.lock',
|
||||
'package-lock.json',
|
||||
];
|
||||
if (req.query.path) {
|
||||
const targetPath = path.join(
|
||||
config.scriptPath,
|
||||
req.query.path as string,
|
||||
);
|
||||
result = readDir(targetPath, config.scriptPath, blacklist);
|
||||
result = await readDir(targetPath, config.scriptPath, blacklist);
|
||||
} else {
|
||||
result = readDirs(config.scriptPath, config.scriptPath, blacklist);
|
||||
result = await readDirs(
|
||||
config.scriptPath,
|
||||
config.scriptPath,
|
||||
blacklist,
|
||||
(a, b) => {
|
||||
if (a.type === b.type) {
|
||||
return a.title.localeCompare(b.title);
|
||||
} else {
|
||||
return a.type === 'directory' ? -1 : 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
res.send({
|
||||
code: 200,
|
||||
@@ -54,17 +72,31 @@ export default (app: Router) => {
|
||||
}
|
||||
});
|
||||
|
||||
route.get(
|
||||
'/detail',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const content = await scriptService.getFile(
|
||||
req.query.path as string,
|
||||
req.query.file as string,
|
||||
);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const filePath = join(
|
||||
config.scriptPath,
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const content = await scriptService.getFile(
|
||||
req.query.path as string,
|
||||
req.params.file,
|
||||
);
|
||||
const content = getFileContentByName(filePath);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -104,12 +136,12 @@ export default (app: Router) => {
|
||||
}
|
||||
|
||||
if (req.file) {
|
||||
fs.renameSync(req.file.path, join(path, req.file.filename));
|
||||
await fs.rename(req.file.path, join(path, filename));
|
||||
return res.send({ code: 200 });
|
||||
}
|
||||
|
||||
if (directory) {
|
||||
fs.mkdirSync(join(path, directory), { recursive: true });
|
||||
await fs.mkdir(join(path, directory), { recursive: true });
|
||||
return res.send({ code: 200 });
|
||||
}
|
||||
|
||||
@@ -121,16 +153,17 @@ export default (app: Router) => {
|
||||
`${originFilename.replace(/\//g, '')}`,
|
||||
);
|
||||
const filePath = join(path, `${filename.replace(/\//g, '')}`);
|
||||
if (fs.existsSync(originFilePath)) {
|
||||
fs.copyFileSync(
|
||||
const fileExists = await fileExist(filePath);
|
||||
if (fileExists) {
|
||||
await fs.copyFile(
|
||||
originFilePath,
|
||||
join(config.bakPath, originFilename.replace(/\//g, '')),
|
||||
);
|
||||
if (filename !== originFilename) {
|
||||
fs.unlinkSync(originFilePath);
|
||||
await rmPath(originFilePath);
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(filePath, content);
|
||||
await fs.writeFile(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -156,7 +189,7 @@ export default (app: Router) => {
|
||||
path: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
fs.writeFileSync(filePath, content);
|
||||
await fs.writeFile(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -182,11 +215,7 @@ export default (app: Router) => {
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
if (type === 'directory') {
|
||||
emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
await rmPath(filePath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -239,7 +268,7 @@ export default (app: Router) => {
|
||||
let { filename, content, path } = req.body;
|
||||
const { name, ext } = parse(filename);
|
||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||
fs.writeFileSync(filePath, content || '', { encoding: 'utf8' });
|
||||
await fs.writeFile(filePath, content || '', { encoding: 'utf8' });
|
||||
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.runScript(filePath);
|
||||
@@ -260,14 +289,17 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, pid } = req.body;
|
||||
const { name, ext } = parse(filename);
|
||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||
const logPath = join(config.logPath, path, `${name}.swap`);
|
||||
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.stopScript(filePath, pid);
|
||||
setTimeout(() => {
|
||||
rmPath(logPath);
|
||||
}, 3000);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -294,7 +326,7 @@ export default (app: Router) => {
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
const newPath = join(config.scriptPath, path, newFilename);
|
||||
fs.renameSync(filePath, newPath);
|
||||
await fs.rename(filePath, newPath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
||||
@@ -210,12 +210,16 @@ export default (app: Router) => {
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
query: Joi.object({
|
||||
force: Joi.boolean().optional(),
|
||||
t: Joi.number()
|
||||
})
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const data = await subscriptionService.remove(req.body);
|
||||
const data = await subscriptionService.remove(req.body, req.query);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
||||
+168
-32
@@ -1,12 +1,11 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import config from '../config';
|
||||
import SystemService from '../services/system';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import UserService from '../services/user';
|
||||
import { EnvModel } from '../data/env';
|
||||
import {
|
||||
getUniqPath,
|
||||
handleLogPath,
|
||||
@@ -14,8 +13,18 @@ import {
|
||||
promiseExec,
|
||||
} from '../config/util';
|
||||
import dayjs from 'dayjs';
|
||||
import multer from 'multer';
|
||||
|
||||
const route = Router();
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, config.tmpPath);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
cb(null, 'data.tgz');
|
||||
},
|
||||
});
|
||||
const upload = multer({ storage: storage });
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/system', route);
|
||||
@@ -25,23 +34,8 @@ export default (app: Router) => {
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const envCount = await EnvModel.count();
|
||||
const { version, changeLog, changeLogLink } = await parseVersion(
|
||||
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', '');
|
||||
const { version, changeLog, changeLogLink, publishTime } =
|
||||
await parseVersion(config.versionFile);
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
@@ -56,9 +50,8 @@ export default (app: Router) => {
|
||||
data: {
|
||||
isInitialized,
|
||||
version,
|
||||
lastCommitTime: dayjs(lastCommitTime).unix(),
|
||||
lastCommitId,
|
||||
branch,
|
||||
publishTime: dayjs(publishTime).unix(),
|
||||
branch: process.env.QL_BRANCH || 'master',
|
||||
changeLog,
|
||||
changeLogLink,
|
||||
},
|
||||
@@ -84,20 +77,16 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config',
|
||||
'/config/log-remove-frequency',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
logRemoveFrequency: Joi.number().optional().allow(null),
|
||||
cronConcurrency: Joi.number().optional().allow(null),
|
||||
logRemoveFrequency: Joi.number().allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateSystemConfig(
|
||||
req.body,
|
||||
);
|
||||
const result = await systemService.updateLogRemoveFrequency(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -105,6 +94,96 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/cron-concurrency',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
cronConcurrency: Joi.number().allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateCronConcurrency(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/dependence-proxy',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
dependenceProxy: Joi.string().allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateDependenceProxy(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/node-mirror',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
nodeMirror: Joi.string().allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
res.setHeader('Content-type', 'application/octet-stream');
|
||||
await systemService.updateNodeMirror(req.body, res);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/python-mirror',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
pythonMirror: Joi.string().allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updatePythonMirror(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/linux-mirror',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
linuxMirror: Joi.string().allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
res.setHeader('Content-type', 'application/octet-stream');
|
||||
await systemService.updateLinuxMirror(req.body, res);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/update-check',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -133,6 +212,25 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/reload',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
type: Joi.string().optional().allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.reloadSystem(req.body.type);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/notify',
|
||||
celebrate({
|
||||
@@ -163,7 +261,10 @@ export default (app: Router) => {
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const uniqPath = await getUniqPath(req.body.command);
|
||||
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');
|
||||
@@ -179,12 +280,12 @@ export default (app: Router) => {
|
||||
onError: async (message: string) => {
|
||||
res.write(`\n${message}`);
|
||||
const absolutePath = await handleLogPath(logPath);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
await fs.appendFile(absolutePath, `\n${message}`);
|
||||
},
|
||||
onLog: async (message: string) => {
|
||||
res.write(`\n${message}`);
|
||||
const absolutePath = await handleLogPath(logPath);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
await fs.appendFile(absolutePath, `\n${message}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -212,4 +313,39 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -74,6 +74,9 @@ export default (app: Router) => {
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
if (process.env.DeployEnv === 'demo') {
|
||||
return res.send({ code: 450, message: '未知错误' });
|
||||
}
|
||||
const userService = Container.get(UserService);
|
||||
await userService.updateUsernameAndPassword(req.body);
|
||||
res.send({ code: 200, message: '更新成功' });
|
||||
|
||||
+3
-2
@@ -2,7 +2,6 @@ import 'reflect-metadata'; // We need this in order to use @Decorators
|
||||
import config from './config';
|
||||
import express from 'express';
|
||||
import Logger from './loaders/logger';
|
||||
import path from 'path';
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
@@ -16,12 +15,14 @@ async function startServer() {
|
||||
await require('./loaders/app').default({ expressApp: app });
|
||||
|
||||
const server = app
|
||||
.listen(config.port, () => {
|
||||
.listen(config.port, '0.0.0.0', () => {
|
||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
||||
console.debug(`✌️ 后端服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,3 +5,21 @@ export const QL_COMMAND = 'ql';
|
||||
|
||||
export const TASK_PREFIX = `${TASK_COMMAND} `;
|
||||
export const QL_PREFIX = `${QL_COMMAND} `;
|
||||
|
||||
export const SAMPLE_FILES = [
|
||||
{
|
||||
title: 'config.sample.sh',
|
||||
value: 'sample/config.sample.sh',
|
||||
target: 'config.sh',
|
||||
},
|
||||
{
|
||||
title: 'notify.js',
|
||||
value: 'sample/notify.js',
|
||||
target: 'data/scripts/sendNotify.js',
|
||||
},
|
||||
{
|
||||
title: 'notify.py',
|
||||
value: 'sample/notify.py',
|
||||
target: 'data/scripts/notify.py',
|
||||
},
|
||||
];
|
||||
|
||||
+27
-2
@@ -1,6 +1,6 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { createRandomString } from './util';
|
||||
import { createRandomString } from './share';
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
@@ -19,7 +19,14 @@ const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
|
||||
const rootPath = process.env.QL_DIR as string;
|
||||
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
let dataPath = path.join(rootPath, 'data/');
|
||||
|
||||
if (process.env.QL_DATA_DIR) {
|
||||
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
|
||||
}
|
||||
|
||||
const shellPath = path.join(rootPath, 'shell/');
|
||||
const tmpPath = path.join(rootPath, '.tmp/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
@@ -28,8 +35,11 @@ const logPath = path.join(dataPath, 'log/');
|
||||
const dbPath = path.join(dataPath, 'db/');
|
||||
const uploadPath = path.join(dataPath, 'upload/');
|
||||
const sshdPath = path.join(dataPath, 'ssh.d/');
|
||||
const systemLogPath = path.join(dataPath, 'syslog/');
|
||||
|
||||
const envFile = path.join(configPath, 'env.sh');
|
||||
const jsEnvFile = path.join(configPath, 'env.js');
|
||||
const pyEnvFile = path.join(configPath, 'env.py');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const crontabFile = path.join(configPath, 'crontab.list');
|
||||
const authConfigFile = path.join(configPath, 'auth.json');
|
||||
@@ -42,6 +52,9 @@ const authError = '错误的用户名密码,请重试';
|
||||
const loginFaild = '请先登录!';
|
||||
const configString = 'config sample crontab shareCode diy';
|
||||
const versionFile = path.join(rootPath, 'version.yaml');
|
||||
const dataTgzFile = path.join(tmpPath, 'data.tgz');
|
||||
const shareShellFile = path.join(shellPath, 'share.sh');
|
||||
const dependenceProxyFile = path.join(configPath, 'dependence-proxy.sh');
|
||||
|
||||
if (envFound.error) {
|
||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||
@@ -51,6 +64,7 @@ export default {
|
||||
port: parseInt(process.env.BACK_PORT as string, 10),
|
||||
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
||||
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
|
||||
updatePort: parseInt(process.env.UPDATE_PORT as string, 10),
|
||||
secret: process.env.SECRET || createRandomString(16, 32),
|
||||
logs: {
|
||||
level: process.env.LOG_LEVEL || 'silly',
|
||||
@@ -59,6 +73,11 @@ export default {
|
||||
prefix: '/api',
|
||||
},
|
||||
rootPath,
|
||||
tmpPath,
|
||||
dataPath,
|
||||
dataTgzFile,
|
||||
shareShellFile,
|
||||
dependenceProxyFile,
|
||||
configString,
|
||||
loginFaild,
|
||||
authError,
|
||||
@@ -70,6 +89,8 @@ export default {
|
||||
sampleFile,
|
||||
confFile,
|
||||
envFile,
|
||||
jsEnvFile,
|
||||
pyEnvFile,
|
||||
dbPath,
|
||||
uploadPath,
|
||||
configPath,
|
||||
@@ -80,7 +101,10 @@ export default {
|
||||
'config.sh.sample',
|
||||
'cookie.sh',
|
||||
'crontab.list',
|
||||
'dependence-proxy.sh',
|
||||
'env.sh',
|
||||
'env.js',
|
||||
'env.py',
|
||||
'token.json',
|
||||
],
|
||||
writePathList: [configPath, scriptPath],
|
||||
@@ -102,4 +126,5 @@ export default {
|
||||
lastVersionFile,
|
||||
sqliteFile,
|
||||
sshdPath,
|
||||
systemLogPath,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { pick } from 'lodash';
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
@@ -32,11 +32,15 @@ export function formatCommand(doc: Subscription, url?: string) {
|
||||
autoDelCron,
|
||||
} = doc;
|
||||
if (type === 'file') {
|
||||
command += `raw "${_url}"`;
|
||||
command += `raw "${_url}" "${proxy || ''}" "${
|
||||
isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||
} else {
|
||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${dependences || ''
|
||||
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
||||
dependences || ''
|
||||
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${
|
||||
isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
+187
-213
@@ -1,4 +1,4 @@
|
||||
import * as fs from 'fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import got from 'got';
|
||||
import iconv from 'iconv-lite';
|
||||
@@ -9,23 +9,28 @@ import { promisify } from 'util';
|
||||
import { load } from 'js-yaml';
|
||||
import config from './index';
|
||||
import { TASK_COMMAND } from './const';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
export function getFileContentByName(fileName: string) {
|
||||
if (fs.existsSync(fileName)) {
|
||||
return fs.readFileSync(fileName, 'utf8');
|
||||
export * from './share';
|
||||
|
||||
export async function getFileContentByName(fileName: string) {
|
||||
const _exsit = await fileExist(fileName);
|
||||
if (_exsit) {
|
||||
return await fs.readFile(fileName, 'utf8');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function getLastModifyFilePath(dir: string) {
|
||||
export async function getLastModifyFilePath(dir: string) {
|
||||
let filePath = '';
|
||||
|
||||
if (fs.existsSync(dir)) {
|
||||
const arr = fs.readdirSync(dir);
|
||||
const _exsit = await fileExist(dir);
|
||||
if (_exsit) {
|
||||
const arr = await fs.readdir(dir);
|
||||
|
||||
arr.forEach((item) => {
|
||||
arr.forEach(async (item) => {
|
||||
const fullpath = path.join(dir, item);
|
||||
const stats = fs.statSync(fullpath);
|
||||
const stats = await fs.lstat(fullpath);
|
||||
if (stats.isFile()) {
|
||||
if (stats.mtimeMs >= 0) {
|
||||
filePath = fullpath;
|
||||
@@ -36,91 +41,6 @@ export function getLastModifyFilePath(dir: string) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
export function createRandomString(min: number, max: number): string {
|
||||
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
const english = [
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'i',
|
||||
'j',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'o',
|
||||
'p',
|
||||
'q',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
'u',
|
||||
'v',
|
||||
'w',
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
];
|
||||
const ENGLISH = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
];
|
||||
const special = ['-', '_'];
|
||||
const config = num.concat(english).concat(ENGLISH).concat(special);
|
||||
|
||||
const arr = [];
|
||||
arr.push(getOne(num));
|
||||
arr.push(getOne(english));
|
||||
arr.push(getOne(ENGLISH));
|
||||
arr.push(getOne(special));
|
||||
|
||||
const len = min + Math.floor(Math.random() * (max - min + 1));
|
||||
|
||||
for (let i = 4; i < len; i++) {
|
||||
arr.push(config[Math.floor(Math.random() * config.length)]);
|
||||
}
|
||||
|
||||
const newArr = [];
|
||||
for (let j = 0; j < len; j++) {
|
||||
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
||||
}
|
||||
|
||||
function getOne(arr: any[]) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
return newArr.join('');
|
||||
}
|
||||
|
||||
export function getToken(req: any) {
|
||||
const { authorization = '' } = req.headers;
|
||||
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
||||
@@ -141,7 +61,8 @@ export async function getNetIp(req: any) {
|
||||
...req.ips,
|
||||
req.socket.remoteAddress,
|
||||
]),
|
||||
];
|
||||
].filter(Boolean);
|
||||
|
||||
let ip = ipArray[0];
|
||||
|
||||
if (ipArray.length > 1) {
|
||||
@@ -162,34 +83,42 @@ export async function getNetIp(req: any) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
|
||||
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
||||
ip = '';
|
||||
}
|
||||
|
||||
if (!ip) {
|
||||
return { address: `获取失败`, ip };
|
||||
}
|
||||
|
||||
try {
|
||||
const baiduApi = got
|
||||
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
||||
const csdnApi = got
|
||||
.get(`https://searchplugin.csdn.net/api/v1/ip/get?ip=${ip}`, {
|
||||
timeout: 10000,
|
||||
retry: 0,
|
||||
})
|
||||
.text();
|
||||
const ipApi = got
|
||||
const pconlineApi = got
|
||||
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
|
||||
timeout: 10000,
|
||||
retry: 0,
|
||||
})
|
||||
.buffer();
|
||||
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
|
||||
|
||||
const ipRegx = /.*IP :(.*)\n/;
|
||||
const addrRegx = /.*数据二 :(.*)\n/;
|
||||
if (data && ipRegx.test(data) && addrRegx.test(data)) {
|
||||
const ip = data.match(ipRegx)[1];
|
||||
const addr = data.match(addrRegx)[1];
|
||||
return { address: addr, ip };
|
||||
} else if (ipApiBody) {
|
||||
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
|
||||
return { address: `${addr}`, ip };
|
||||
} else {
|
||||
return { address: `获取失败`, ip };
|
||||
const [csdnBody, pconlineBody] = await await Promise.all<any>([
|
||||
csdnApi,
|
||||
pconlineApi,
|
||||
]);
|
||||
const csdnRes = JSON.parse(csdnBody);
|
||||
const pconlineRes = JSON.parse(iconv.decode(pconlineBody, 'GBK'));
|
||||
let address = '';
|
||||
if (csdnBody && csdnRes.code == 200) {
|
||||
address = csdnRes.data.address;
|
||||
} else if (pconlineRes && pconlineRes.addr) {
|
||||
address = pconlineRes.addr;
|
||||
}
|
||||
return { address, ip };
|
||||
} catch (error) {
|
||||
return { address: `获取失败`, ip };
|
||||
}
|
||||
@@ -229,22 +158,17 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
||||
}
|
||||
|
||||
export async function fileExist(file: any) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
fs.accessSync(file);
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
try {
|
||||
await fs.access(file);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createFile(file: string, data: string = '') {
|
||||
return new Promise((resolve) => {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, data);
|
||||
resolve(true);
|
||||
});
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
await fs.writeFile(file, data);
|
||||
}
|
||||
|
||||
export async function handleLogPath(
|
||||
@@ -307,61 +231,76 @@ interface IFile {
|
||||
type: 'directory' | 'file';
|
||||
parent: string;
|
||||
mtime: number;
|
||||
size?: number;
|
||||
children?: IFile[];
|
||||
}
|
||||
|
||||
export function dirSort(a: IFile, b: IFile) {
|
||||
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1;
|
||||
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1;
|
||||
export function dirSort(a: IFile, b: IFile): number {
|
||||
if (a.type === 'file' && b.type === 'file') {
|
||||
return b.mtime - a.mtime;
|
||||
} else if (a.type === 'directory' && b.type === 'directory') {
|
||||
return a.title.localeCompare(b.title);
|
||||
} else {
|
||||
return a.type === 'directory' ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function readDirs(
|
||||
export async function readDirs(
|
||||
dir: string,
|
||||
baseDir: string = '',
|
||||
blacklist: string[] = [],
|
||||
) {
|
||||
sort: (a: IFile, b: IFile) => number = dirSort,
|
||||
): Promise<IFile[]> {
|
||||
const relativePath = path.relative(baseDir, dir);
|
||||
const files = fs.readdirSync(dir);
|
||||
const result: any = files
|
||||
.filter((x) => !blacklist.includes(x))
|
||||
.map((file: string) => {
|
||||
const subPath = path.join(dir, file);
|
||||
const stats = fs.statSync(subPath);
|
||||
const key = path.join(relativePath, file);
|
||||
if (stats.isDirectory()) {
|
||||
return {
|
||||
title: file,
|
||||
key,
|
||||
type: 'directory',
|
||||
parent: relativePath,
|
||||
mtime: stats.mtime.getTime(),
|
||||
children: readDirs(subPath, baseDir).sort(dirSort),
|
||||
};
|
||||
}
|
||||
return {
|
||||
const files = await fs.readdir(dir);
|
||||
const result: IFile[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const subPath = path.join(dir, file);
|
||||
const stats = await fs.lstat(subPath);
|
||||
const key = path.join(relativePath, file);
|
||||
|
||||
if (blacklist.includes(file) || stats.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const children = await readDirs(subPath, baseDir, blacklist, sort);
|
||||
result.push({
|
||||
title: file,
|
||||
type: 'file',
|
||||
isLeaf: true,
|
||||
key,
|
||||
type: 'directory',
|
||||
parent: relativePath,
|
||||
mtime: stats.mtime.getTime(),
|
||||
};
|
||||
});
|
||||
return result.sort(dirSort);
|
||||
children: children.sort(sort),
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
title: file,
|
||||
type: 'file',
|
||||
key,
|
||||
parent: relativePath,
|
||||
size: stats.size,
|
||||
mtime: stats.mtime.getTime(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result.sort(sort);
|
||||
}
|
||||
|
||||
export function readDir(
|
||||
export async function readDir(
|
||||
dir: string,
|
||||
baseDir: string = '',
|
||||
blacklist: string[] = [],
|
||||
) {
|
||||
const relativePath = path.relative(baseDir, dir);
|
||||
const files = fs.readdirSync(dir);
|
||||
const files = await fs.readdir(dir);
|
||||
const result: any = files
|
||||
.filter((x) => !blacklist.includes(x))
|
||||
.map((file: string) => {
|
||||
.map(async (file: string) => {
|
||||
const subPath = path.join(dir, file);
|
||||
const stats = fs.statSync(subPath);
|
||||
const stats = await fs.lstat(subPath);
|
||||
const key = path.join(relativePath, file);
|
||||
return {
|
||||
title: file,
|
||||
@@ -373,30 +312,28 @@ export function readDir(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function emptyDir(path: string) {
|
||||
const files = fs.readdirSync(path);
|
||||
files.forEach((file) => {
|
||||
const filePath = `${path}/${file}`;
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(path);
|
||||
export async function promiseExec(command: string): Promise<string> {
|
||||
try {
|
||||
const { stderr, stdout } = await promisify(exec)(command, {
|
||||
maxBuffer: 200 * 1024 * 1024,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return stdout || stderr;
|
||||
} catch (error) {
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
}
|
||||
|
||||
export function promiseExec(command: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
command,
|
||||
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
|
||||
(err, stdout, stderr) => {
|
||||
resolve(stdout || stderr || JSON.stringify(err));
|
||||
},
|
||||
);
|
||||
});
|
||||
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) {
|
||||
@@ -423,37 +360,49 @@ export function parseHeaders(headers: string) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseString(
|
||||
input: string,
|
||||
valueFormatFn?: (v: string) => string,
|
||||
): Record<string, string> {
|
||||
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
|
||||
const matches: Record<string, string> = {};
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(input)) !== null) {
|
||||
const [, key, value] = match;
|
||||
const _key = key.trim();
|
||||
if (!_key || matches[_key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let _value = value.trim();
|
||||
|
||||
try {
|
||||
_value = valueFormatFn ? valueFormatFn(_value) : _value;
|
||||
const jsonValue = JSON.parse(_value);
|
||||
matches[_key] = jsonValue;
|
||||
} catch (error) {
|
||||
matches[_key] = _value;
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function parseBody(
|
||||
body: string,
|
||||
contentType:
|
||||
| 'application/json'
|
||||
| 'multipart/form-data'
|
||||
| 'application/x-www-form-urlencoded',
|
||||
| 'application/x-www-form-urlencoded'
|
||||
| 'text/plain',
|
||||
valueFormatFn?: (v: string) => string,
|
||||
) {
|
||||
if (!body) return '';
|
||||
if (contentType === 'text/plain' || !body) {
|
||||
return valueFormatFn && body ? valueFormatFn(body) : body;
|
||||
}
|
||||
|
||||
const parsed: any = {};
|
||||
let key;
|
||||
let val;
|
||||
let i;
|
||||
|
||||
body &&
|
||||
body.split('\n').forEach(function parser(line) {
|
||||
i = line.indexOf(':');
|
||||
key = line.substring(0, i).trim().toLowerCase();
|
||||
val = line.substring(i + 1).trim();
|
||||
|
||||
if (!key || parsed[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonValue = JSON.parse(val);
|
||||
parsed[key] = jsonValue;
|
||||
} catch (error) {
|
||||
parsed[key] = val;
|
||||
}
|
||||
});
|
||||
const parsed = parseString(body, valueFormatFn);
|
||||
|
||||
switch (contentType) {
|
||||
case 'multipart/form-data':
|
||||
@@ -483,11 +432,11 @@ export function psTree(pid: number): Promise<number[]> {
|
||||
|
||||
export async function killTask(pid: number) {
|
||||
const pids = await psTree(pid);
|
||||
// SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
|
||||
|
||||
if (pids.length) {
|
||||
try {
|
||||
[pid, ...pids].forEach((x) => {
|
||||
process.kill(x, 2);
|
||||
[pid, ...pids].reverse().forEach((x) => {
|
||||
process.kill(x, 15);
|
||||
});
|
||||
} catch (error) {}
|
||||
} else {
|
||||
@@ -495,8 +444,8 @@ export async function killTask(pid: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPid(name: string) {
|
||||
const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
|
||||
export async function getPid(cmd: string) {
|
||||
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
|
||||
const pid = await promiseExec(taskCommand);
|
||||
return pid ? Number(pid) : undefined;
|
||||
}
|
||||
@@ -505,21 +454,22 @@ interface IVersion {
|
||||
version: string;
|
||||
changeLogLink: string;
|
||||
changeLog: string;
|
||||
publishTime: string;
|
||||
}
|
||||
|
||||
export async function parseVersion(path: string): Promise<IVersion> {
|
||||
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
|
||||
return load(await fs.readFile(path, 'utf8')) as IVersion;
|
||||
}
|
||||
|
||||
export async function parseContentVersion(content: string): Promise<IVersion> {
|
||||
return load(content) as IVersion;
|
||||
}
|
||||
|
||||
export async function getUniqPath(command: string): Promise<string> {
|
||||
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
|
||||
let id = await promiseExec(idStr);
|
||||
|
||||
if (/^\d\d*\d$/.test(id)) {
|
||||
export async function getUniqPath(
|
||||
command: string,
|
||||
id: string,
|
||||
): Promise<string> {
|
||||
if (/^\d+$/.test(id)) {
|
||||
id = `_${id}`;
|
||||
} else {
|
||||
id = '';
|
||||
@@ -551,3 +501,27 @@ export async function getUniqPath(command: string): Promise<string> {
|
||||
|
||||
return `${str}${id}`;
|
||||
}
|
||||
|
||||
export function safeJSONParse(value?: string) {
|
||||
if (!value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
Logger.error('[JSON.parse失败]', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function rmPath(path: string) {
|
||||
try {
|
||||
const _exsit = await fileExist(path);
|
||||
if (_exsit) {
|
||||
await fs.rm(path, { force: true, recursive: true, maxRetries: 5 });
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[rmPath失败]', error);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -18,10 +18,13 @@ export class Crontab {
|
||||
last_running_time?: number;
|
||||
last_execution_time?: number;
|
||||
sub_id?: number;
|
||||
extra_schedules?: Array<{ schedule: string }>;
|
||||
task_before?: string;
|
||||
task_after?: string;
|
||||
|
||||
constructor(options: Crontab) {
|
||||
this.name = options.name;
|
||||
this.command = options.command;
|
||||
this.command = options.command.trim();
|
||||
this.schedule = options.schedule;
|
||||
this.saved = options.saved;
|
||||
this.id = options.id;
|
||||
@@ -39,6 +42,9 @@ export class Crontab {
|
||||
this.last_running_time = options.last_running_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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +55,7 @@ export enum CrontabStatus {
|
||||
'disabled',
|
||||
}
|
||||
|
||||
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
||||
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { }
|
||||
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||
name: {
|
||||
unique: 'compositeIndex',
|
||||
@@ -75,4 +81,7 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||
last_running_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>,
|
||||
CrontabView {}
|
||||
export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
||||
|
||||
+21
-8
@@ -4,9 +4,9 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
export class Dependence {
|
||||
timestamp?: string;
|
||||
id?: number;
|
||||
status?: DependenceStatus;
|
||||
type?: DependenceTypes;
|
||||
name?: number;
|
||||
status: DependenceStatus;
|
||||
type: DependenceTypes;
|
||||
name: string;
|
||||
log?: string[];
|
||||
remark?: string;
|
||||
|
||||
@@ -18,7 +18,7 @@ export class Dependence {
|
||||
: DependenceStatus.queued;
|
||||
this.type = options.type || DependenceTypes.nodejs;
|
||||
this.timestamp = new Date().toString();
|
||||
this.name = options.name;
|
||||
this.name = options.name.trim();
|
||||
this.log = options.log || [];
|
||||
this.remark = options.remark || '';
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export enum DependenceStatus {
|
||||
'removed',
|
||||
'removeFailed',
|
||||
'queued',
|
||||
'cancelled',
|
||||
}
|
||||
|
||||
export enum DependenceTypes {
|
||||
@@ -42,17 +43,29 @@ export enum DependenceTypes {
|
||||
|
||||
export enum InstallDependenceCommandTypes {
|
||||
'pnpm add -g',
|
||||
'pip3 install',
|
||||
'apk add',
|
||||
'pip3 install --disable-pip-version-check --root-user-action=ignore',
|
||||
'apk add --no-check-certificate',
|
||||
}
|
||||
|
||||
export enum GetDependenceCommandTypes {
|
||||
'pnpm ls -g ',
|
||||
'pip3 show --disable-pip-version-check',
|
||||
'apk info -es',
|
||||
}
|
||||
|
||||
export enum versionDependenceCommandTypes {
|
||||
'@',
|
||||
'==',
|
||||
'=',
|
||||
}
|
||||
|
||||
export enum unInstallDependenceCommandTypes {
|
||||
'pnpm remove -g',
|
||||
'pip3 uninstall -y',
|
||||
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
||||
'apk del',
|
||||
}
|
||||
|
||||
interface DependenceInstance
|
||||
export interface DependenceInstance
|
||||
extends Model<Dependence, Dependence>,
|
||||
Dependence {}
|
||||
export const DependenceModel = sequelize.define<DependenceInstance>(
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ export const initPosition = 4500000000000000;
|
||||
export const stepPosition = 10000000000;
|
||||
export const minPosition = 100;
|
||||
|
||||
interface EnvInstance extends Model<Env, Env>, Env {}
|
||||
export interface EnvInstance extends Model<Env, Env>, Env {}
|
||||
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||
timestamp: DataTypes.STRING,
|
||||
|
||||
+28
-1
@@ -14,9 +14,12 @@ export enum NotificationMode {
|
||||
'aibotk' = 'aibotk',
|
||||
'iGot' = 'iGot',
|
||||
'pushPlus' = 'pushPlus',
|
||||
'wePlusBot' = 'wePlusBot',
|
||||
'email' = 'email',
|
||||
'pushMe' = 'pushMe',
|
||||
'feishu' = 'feishu',
|
||||
'webhook' = 'webhook',
|
||||
'chronocat' = 'Chronocat',
|
||||
}
|
||||
|
||||
abstract class NotificationBaseInfo {
|
||||
@@ -54,6 +57,9 @@ export class BarkNotification extends NotificationBaseInfo {
|
||||
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
||||
public barkSound = '';
|
||||
public barkGroup = 'qinglong';
|
||||
public barkLevel = 'active';
|
||||
public barkUrl = '';
|
||||
public barkArchive=""
|
||||
}
|
||||
|
||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||
@@ -62,7 +68,7 @@ export class TelegramBotNotification extends NotificationBaseInfo {
|
||||
public telegramBotProxyHost = '';
|
||||
public telegramBotProxyPort = '';
|
||||
public telegramBotProxyAuth = '';
|
||||
public telegramBotApiHost = 'api.telegram.org';
|
||||
public telegramBotApiHost = 'https://api.telegram.org';
|
||||
}
|
||||
|
||||
export class DingtalkBotNotification extends NotificationBaseInfo {
|
||||
@@ -95,12 +101,29 @@ export class PushPlusNotification extends NotificationBaseInfo {
|
||||
public pushPlusUser = '';
|
||||
}
|
||||
|
||||
export class WePlusBotNotification extends NotificationBaseInfo {
|
||||
public wePlusBotToken = '';
|
||||
public wePlusBotReceiver = '';
|
||||
public wePlusBotVersion = '';
|
||||
}
|
||||
|
||||
export class EmailNotification extends NotificationBaseInfo {
|
||||
public emailService: string = '';
|
||||
public emailUser: string = '';
|
||||
public emailPass: string = '';
|
||||
}
|
||||
|
||||
export class PushMeNotification extends NotificationBaseInfo {
|
||||
public pushMeKey: string = '';
|
||||
public pushMeUrl: string = '';
|
||||
}
|
||||
|
||||
export class ChronocatNotification extends NotificationBaseInfo {
|
||||
public chronocatURL: string = '';
|
||||
public chronocatQQ: string = '';
|
||||
public chronocatToken: string = '';
|
||||
}
|
||||
|
||||
export class WebhookNotification extends NotificationBaseInfo {
|
||||
public webhookHeaders: string = '';
|
||||
public webhookBody: string = '';
|
||||
@@ -130,6 +153,10 @@ export interface NotificationInfo
|
||||
AibotkNotification,
|
||||
IGotNotification,
|
||||
PushPlusNotification,
|
||||
WePlusBotNotification,
|
||||
EmailNotification,
|
||||
PushMeNotification,
|
||||
WebhookNotification,
|
||||
ChronocatNotification,
|
||||
LarkNotification {}
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export interface AppToken {
|
||||
|
||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
||||
|
||||
interface AppInstance extends Model<App, App>, App {}
|
||||
export interface AppInstance extends Model<App, App>, App {}
|
||||
export const AppModel = sequelize.define<AppInstance>('App', {
|
||||
name: { type: DataTypes.STRING, unique: 'name' },
|
||||
scopes: DataTypes.JSON,
|
||||
|
||||
+4
-1
@@ -16,4 +16,7 @@ export type SockMessageType =
|
||||
| 'uninstallDependence'
|
||||
| 'updateSystemVersion'
|
||||
| 'manuallyRunScript'
|
||||
| 'runSubscriptionEnd';
|
||||
| 'runSubscriptionEnd'
|
||||
| 'reloadSystem'
|
||||
| 'updateNodeMirror'
|
||||
| 'updateLinuxMirror';
|
||||
|
||||
@@ -70,7 +70,7 @@ export enum SubscriptionStatus {
|
||||
'queued',
|
||||
}
|
||||
|
||||
interface SubscriptionInstance
|
||||
export interface SubscriptionInstance
|
||||
extends Model<Subscription, Subscription>,
|
||||
Subscription {}
|
||||
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
||||
|
||||
@@ -2,13 +2,13 @@ import { sequelize } from '.';
|
||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
import { NotificationInfo } from './notify';
|
||||
|
||||
export class AuthInfo {
|
||||
export class SystemInfo {
|
||||
ip?: string;
|
||||
type: AuthDataType;
|
||||
info?: AuthModelInfo;
|
||||
info?: SystemModelInfo;
|
||||
id?: number;
|
||||
|
||||
constructor(options: AuthInfo) {
|
||||
constructor(options: SystemInfo) {
|
||||
this.ip = options.ip;
|
||||
this.info = options.info;
|
||||
this.type = options.type;
|
||||
@@ -32,6 +32,10 @@ export enum AuthDataType {
|
||||
export interface SystemConfigInfo {
|
||||
logRemoveFrequency?: number;
|
||||
cronConcurrency?: number;
|
||||
dependenceProxy?: string;
|
||||
nodeMirror?: string;
|
||||
pythonMirror?: string;
|
||||
linuxMirror?: string;
|
||||
}
|
||||
|
||||
export interface LoginLogInfo {
|
||||
@@ -39,13 +43,15 @@ export interface LoginLogInfo {
|
||||
address?: string;
|
||||
ip?: string;
|
||||
platform?: string;
|
||||
status?: LoginStatus,
|
||||
status?: LoginStatus;
|
||||
}
|
||||
|
||||
export type AuthModelInfo = SystemConfigInfo & Partial<NotificationInfo> & LoginLogInfo;
|
||||
export type SystemModelInfo = SystemConfigInfo &
|
||||
Partial<NotificationInfo> &
|
||||
LoginLogInfo;
|
||||
|
||||
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo { }
|
||||
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
|
||||
export interface SystemInstance extends Model<SystemInfo, SystemInfo>, SystemInfo { }
|
||||
export const SystemModel = sequelize.define<SystemInstance>('Auth', {
|
||||
ip: DataTypes.STRING,
|
||||
type: DataTypes.STRING,
|
||||
info: {
|
||||
Vendored
+2
@@ -3,3 +3,5 @@ declare namespace Express {
|
||||
platform: 'desktop' | 'mobile';
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'pstree.remy';
|
||||
|
||||
+13
-8
@@ -7,18 +7,23 @@ import linkDeps from './deps';
|
||||
import initTask from './initTask';
|
||||
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
await depInjectorLoader();
|
||||
Logger.info('✌️ Dependency Injector loaded');
|
||||
|
||||
await expressLoader({ app: expressApp });
|
||||
Logger.info('✌️ Express loaded');
|
||||
depInjectorLoader();
|
||||
Logger.info('✌️ Dependency loaded');
|
||||
console.log('✌️ Dependency loaded');
|
||||
|
||||
await initData();
|
||||
Logger.info('✌️ init data loaded');
|
||||
Logger.info('✌️ Init data loaded');
|
||||
console.log('✌️ Init data loaded');
|
||||
|
||||
await linkDeps();
|
||||
Logger.info('✌️ link deps loaded');
|
||||
Logger.info('✌️ Link deps loaded');
|
||||
console.log('✌️ Link deps loaded');
|
||||
|
||||
initTask();
|
||||
Logger.info('✌️ init task loaded');
|
||||
Logger.info('✌️ Init task loaded');
|
||||
console.log('✌️ Init task loaded');
|
||||
|
||||
expressLoader({ app: expressApp });
|
||||
Logger.info('✌️ Express loaded');
|
||||
console.log('✌️ Express loaded');
|
||||
};
|
||||
|
||||
+17
-7
@@ -5,7 +5,7 @@ import { EnvModel } from '../data/env';
|
||||
import { CrontabModel } from '../data/cron';
|
||||
import { DependenceModel } from '../data/dependence';
|
||||
import { AppModel } from '../data/open';
|
||||
import { AuthModel } from '../data/auth';
|
||||
import { SystemModel } from '../data/system';
|
||||
import { fileExist } from '../config/util';
|
||||
import { SubscriptionModel } from '../data/subscription';
|
||||
import { CrontabViewModel } from '../data/cronView';
|
||||
@@ -17,7 +17,7 @@ export default async () => {
|
||||
await CrontabModel.sync();
|
||||
await DependenceModel.sync();
|
||||
await AppModel.sync();
|
||||
await AuthModel.sync();
|
||||
await SystemModel.sync();
|
||||
await EnvModel.sync();
|
||||
await SubscriptionModel.sync();
|
||||
await CrontabViewModel.sync();
|
||||
@@ -48,7 +48,16 @@ export default async () => {
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
|
||||
} catch (error) {}
|
||||
} 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 升级
|
||||
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
||||
@@ -66,7 +75,7 @@ export default async () => {
|
||||
const dependenceCount = await DependenceModel.count();
|
||||
const envCount = await EnvModel.count();
|
||||
const appCount = await AppModel.count();
|
||||
const authCount = await AuthModel.count();
|
||||
const authCount = await SystemModel.count();
|
||||
if (crondbExist && cronCount === 0) {
|
||||
const cronDb = new DataStore({
|
||||
filename: cronDbFile,
|
||||
@@ -118,13 +127,14 @@ export default async () => {
|
||||
});
|
||||
authDb.persistence.compactDatafile();
|
||||
authDb.find({}).exec(async (err, docs) => {
|
||||
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
await SystemModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✌️ DB loaded');
|
||||
Logger.info('✌️ DB loaded');
|
||||
} catch (error) {
|
||||
Logger.info('✌️ DB load failed');
|
||||
Logger.info(error);
|
||||
console.error('✌️ DB load failed');
|
||||
Logger.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
+29
-20
@@ -1,49 +1,58 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import chokidar from 'chokidar';
|
||||
import config from '../config/index';
|
||||
import { promiseExec } from '../config/util';
|
||||
import { fileExist, promiseExec, rmPath } from '../config/util';
|
||||
|
||||
function linkToNodeModule(src: string, dst?: string) {
|
||||
async function linkToNodeModule(src: string, dst?: string) {
|
||||
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
||||
const source = path.join(config.rootPath, src);
|
||||
|
||||
fs.lstat(target, (err, stat) => {
|
||||
if (!stat) {
|
||||
fs.symlink(source, target, 'dir', (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
try {
|
||||
const stats = await fs.lstat(target);
|
||||
if (!stats) {
|
||||
await fs.symlink(source, target, 'dir');
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
async function linkCommand() {
|
||||
const commandPath = await promiseExec('which node');
|
||||
const commandDir = path.dirname(commandPath);
|
||||
const linkShell = [
|
||||
const oldLinkShell = [
|
||||
{
|
||||
src: 'update.sh',
|
||||
dest: 'ql',
|
||||
},
|
||||
{
|
||||
src: 'task.sh',
|
||||
dest: 'task',
|
||||
tmp: 'ql_tmp',
|
||||
},
|
||||
];
|
||||
// const newLinkShell = [
|
||||
// {
|
||||
// src: 'task.mjs',
|
||||
// dest: 'task',
|
||||
// tmp: 'task_tmp',
|
||||
// },
|
||||
// ];
|
||||
|
||||
for (const link of linkShell) {
|
||||
for (const link of oldLinkShell) {
|
||||
const source = path.join(config.rootPath, 'shell', link.src);
|
||||
const target = path.join(commandDir, link.dest);
|
||||
if (fs.existsSync(target)) {
|
||||
fs.unlinkSync(target);
|
||||
}
|
||||
fs.symlink(source, target, (err) => {});
|
||||
const tmpTarget = path.join(commandDir, link.tmp);
|
||||
await fs.symlink(source, tmpTarget);
|
||||
await fs.rename(tmpTarget, target);
|
||||
}
|
||||
// for (const link of newLinkShell) {
|
||||
// const source = path.join(config.rootPath, 'cli', link.src);
|
||||
// const target = path.join(commandDir, link.dest);
|
||||
// const tmpTarget = path.join(commandDir, link.tmp);
|
||||
// await fs.symlink(source, tmpTarget);
|
||||
// await fs.rename(tmpTarget, target);
|
||||
// }
|
||||
}
|
||||
|
||||
export default async (src: string = 'deps') => {
|
||||
await linkCommand();
|
||||
linkToNodeModule(src);
|
||||
await linkToNodeModule(src);
|
||||
|
||||
const source = path.join(config.rootPath, src);
|
||||
const watcher = chokidar.watch(source, {
|
||||
|
||||
@@ -4,8 +4,8 @@ import cors from 'cors';
|
||||
import routes from '../api';
|
||||
import config from '../config';
|
||||
import jwt, { UnauthorizedError } from 'express-jwt';
|
||||
import fs from 'fs';
|
||||
import { getPlatform, getToken } from '../config/util';
|
||||
import fs from 'fs/promises';
|
||||
import { getPlatform, getToken, safeJSONParse } from '../config/util';
|
||||
import Container from 'typedi';
|
||||
import OpenService from '../services/open';
|
||||
import rewrite from 'express-urlrewrite';
|
||||
@@ -15,9 +15,10 @@ import { EnvModel } from '../data/env';
|
||||
import { errors } from 'celebrate';
|
||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
import { serveEnv } from '../config/serverEnv';
|
||||
import Logger from './logger';
|
||||
|
||||
export default ({ app }: { app: Application }) => {
|
||||
app.enable('trust proxy');
|
||||
app.set('trust proxy', 'loopback');
|
||||
app.use(cors());
|
||||
app.get(`${config.api.prefix}/env.js`, serveEnv);
|
||||
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
||||
@@ -25,9 +26,10 @@ export default ({ app }: { app: Application }) => {
|
||||
app.use(
|
||||
'/api/public',
|
||||
createProxyMiddleware({
|
||||
target: `http://localhost:${config.publicPort}/api`,
|
||||
target: `http://0.0.0.0:${config.publicPort}/api`,
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '/api/public': '' },
|
||||
logProvider: () => Logger,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -36,7 +38,7 @@ export default ({ app }: { app: Application }) => {
|
||||
|
||||
app.use(
|
||||
jwt({
|
||||
secret: config.secret as string,
|
||||
secret: config.secret,
|
||||
algorithms: ['HS384'],
|
||||
}).unless({
|
||||
path: [...config.apiWhiteList, /^\/open\//],
|
||||
@@ -81,9 +83,9 @@ export default ({ app }: { app: Application }) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
const data = await fs.readFile(config.authConfigFile, 'utf8');
|
||||
if (data && headerToken) {
|
||||
const { token = '', tokens = {} } = JSON.parse(data);
|
||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||
return next();
|
||||
}
|
||||
@@ -103,7 +105,6 @@ export default ({ app }: { app: Application }) => {
|
||||
}
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const envCount = await EnvModel.count();
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
|
||||
+55
-20
@@ -9,11 +9,62 @@ import { Op } from 'sequelize';
|
||||
import config from '../config';
|
||||
import { CrontabViewModel, CronViewType } from '../data/cronView';
|
||||
import { initPosition } from '../data/env';
|
||||
import { AuthDataType, SystemModel } from '../data/system';
|
||||
import SystemService from '../services/system';
|
||||
|
||||
export default async () => {
|
||||
const cronService = Container.get(CronService);
|
||||
const envService = Container.get(EnvService);
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const systemService = Container.get(SystemService);
|
||||
|
||||
// 初始化增加系统配置
|
||||
await SystemModel.upsert({ type: AuthDataType.systemConfig });
|
||||
await SystemModel.upsert({ type: AuthDataType.notification });
|
||||
|
||||
const installDependencies = () => {
|
||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||
DependenceModel.findAll({
|
||||
where: {},
|
||||
order: [
|
||||
['type', 'DESC'],
|
||||
['createdAt', 'DESC'],
|
||||
],
|
||||
raw: true,
|
||||
}).then(async (docs) => {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.queued, log: [] },
|
||||
{ where: { id: docs.map((x) => x.id!) } },
|
||||
);
|
||||
setTimeout(() => {
|
||||
dependenceService.installDependenceOneByOne(docs);
|
||||
}, 5000);
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化更新 linux/python/nodejs 镜像源配置
|
||||
const systemConfig = await systemService.getSystemConfig();
|
||||
if (systemConfig.info?.pythonMirror) {
|
||||
systemService.updatePythonMirror({
|
||||
pythonMirror: systemConfig.info?.pythonMirror,
|
||||
});
|
||||
}
|
||||
if (systemConfig.info?.linuxMirror) {
|
||||
systemService.updateLinuxMirror(
|
||||
{
|
||||
linuxMirror: systemConfig.info?.linuxMirror,
|
||||
},
|
||||
undefined,
|
||||
() => installDependencies(),
|
||||
);
|
||||
} else {
|
||||
installDependencies();
|
||||
}
|
||||
if (systemConfig.info?.nodeMirror) {
|
||||
systemService.updateNodeMirror({
|
||||
nodeMirror: systemConfig.info?.nodeMirror,
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化新增默认全部任务视图
|
||||
CrontabViewModel.findAll({
|
||||
@@ -30,23 +81,7 @@ export default async () => {
|
||||
});
|
||||
|
||||
// 初始化更新所有任务状态为空闲
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle },
|
||||
{ where: {} },
|
||||
);
|
||||
|
||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||
DependenceModel.findAll({
|
||||
where: {},
|
||||
order: [['type', 'DESC']],
|
||||
raw: true,
|
||||
}).then(async (docs) => {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.queued, log: [] },
|
||||
{ where: { id: docs.map((x) => x.id!) } },
|
||||
);
|
||||
dependenceService.installDependenceOneByOne(docs);
|
||||
});
|
||||
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
|
||||
|
||||
// 初始化时执行一次所有的 ql repo 任务
|
||||
CrontabModel.findAll({
|
||||
@@ -90,7 +125,7 @@ export default async () => {
|
||||
if (doc.command.includes(`${config.rootPath}/log/`)) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
command: `${config.rootPath}/data/log/${doc.command.replace(
|
||||
command: `${config.dataPath}/log/${doc.command.replace(
|
||||
`${config.rootPath}/log/`,
|
||||
'',
|
||||
)}`,
|
||||
@@ -101,7 +136,7 @@ export default async () => {
|
||||
if (doc.command.includes(`${config.rootPath}/config/`)) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
command: `${config.rootPath}/data/config/${doc.command.replace(
|
||||
command: `${config.dataPath}/config/${doc.command.replace(
|
||||
`${config.rootPath}/config/`,
|
||||
'',
|
||||
)}`,
|
||||
@@ -112,7 +147,7 @@ export default async () => {
|
||||
if (doc.command.includes(`${config.rootPath}/db/`)) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
command: `${config.rootPath}/data/db/${doc.command.replace(
|
||||
command: `${config.dataPath}/db/${doc.command.replace(
|
||||
`${config.rootPath}/db/`,
|
||||
'',
|
||||
)}`,
|
||||
|
||||
+35
-20
@@ -1,12 +1,16 @@
|
||||
import fs from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import dotenv from 'dotenv';
|
||||
import Logger from './logger';
|
||||
import { fileExist } from '../config/util';
|
||||
|
||||
const rootPath = process.env.QL_DIR as string;;
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
const rootPath = process.env.QL_DIR as string;
|
||||
let dataPath = path.join(rootPath, 'data/');
|
||||
|
||||
if (process.env.QL_DATA_DIR) {
|
||||
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
|
||||
}
|
||||
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
const logPath = path.join(dataPath, 'log/');
|
||||
@@ -28,6 +32,7 @@ const TaskAfterFile = path.join(configPath, 'task_after.sh');
|
||||
const homedir = os.homedir();
|
||||
const sshPath = path.resolve(homedir, '.ssh');
|
||||
const sshdPath = path.join(dataPath, 'ssh.d');
|
||||
const systemLogPath = path.join(dataPath, 'syslog');
|
||||
|
||||
export default async () => {
|
||||
const authFileExist = await fileExist(authConfigFile);
|
||||
@@ -39,6 +44,7 @@ export default async () => {
|
||||
const sshDirExist = await fileExist(sshPath);
|
||||
const bakDirExist = await fileExist(bakPath);
|
||||
const sshdDirExist = await fileExist(sshdPath);
|
||||
const systemLogDirExist = await fileExist(systemLogPath);
|
||||
const tmpDirExist = await fileExist(tmpPath);
|
||||
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
|
||||
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
|
||||
@@ -46,63 +52,72 @@ export default async () => {
|
||||
const TaskAfterFileExist = await fileExist(TaskAfterFile);
|
||||
|
||||
if (!configDirExist) {
|
||||
fs.mkdirSync(configPath);
|
||||
await fs.mkdir(configPath);
|
||||
}
|
||||
|
||||
if (!scriptDirExist) {
|
||||
fs.mkdirSync(scriptPath);
|
||||
await fs.mkdir(scriptPath);
|
||||
}
|
||||
|
||||
if (!logDirExist) {
|
||||
fs.mkdirSync(logPath);
|
||||
await fs.mkdir(logPath);
|
||||
}
|
||||
|
||||
if (!tmpDirExist) {
|
||||
fs.mkdirSync(tmpPath);
|
||||
await fs.mkdir(tmpPath);
|
||||
}
|
||||
|
||||
if (!uploadDirExist) {
|
||||
fs.mkdirSync(uploadPath);
|
||||
await fs.mkdir(uploadPath);
|
||||
}
|
||||
|
||||
if (!sshDirExist) {
|
||||
fs.mkdirSync(sshPath);
|
||||
await fs.mkdir(sshPath);
|
||||
}
|
||||
|
||||
if (!bakDirExist) {
|
||||
fs.mkdirSync(bakPath);
|
||||
await fs.mkdir(bakPath);
|
||||
}
|
||||
|
||||
if (!sshdDirExist) {
|
||||
fs.mkdirSync(sshdPath);
|
||||
await fs.mkdir(sshdPath);
|
||||
}
|
||||
|
||||
if (!systemLogDirExist) {
|
||||
await fs.mkdir(systemLogPath);
|
||||
}
|
||||
|
||||
// 初始化文件
|
||||
if (!authFileExist) {
|
||||
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||
await fs.writeFile(authConfigFile, await fs.readFile(sampleAuthFile));
|
||||
}
|
||||
|
||||
if (!confFileExist) {
|
||||
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
||||
await fs.writeFile(confFile, await fs.readFile(sampleConfigFile));
|
||||
}
|
||||
|
||||
if (!scriptNotifyJsFileExist) {
|
||||
fs.writeFileSync(scriptNotifyJsFile, fs.readFileSync(sampleNotifyJsFile));
|
||||
await fs.writeFile(
|
||||
scriptNotifyJsFile,
|
||||
await fs.readFile(sampleNotifyJsFile),
|
||||
);
|
||||
}
|
||||
|
||||
if (!scriptNotifyPyFileExist) {
|
||||
fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile));
|
||||
await fs.writeFile(
|
||||
scriptNotifyPyFile,
|
||||
await fs.readFile(sampleNotifyPyFile),
|
||||
);
|
||||
}
|
||||
|
||||
if (!TaskBeforeFileExist) {
|
||||
fs.writeFileSync(TaskBeforeFile, fs.readFileSync(sampleTaskShellFile));
|
||||
await fs.writeFile(TaskBeforeFile, await fs.readFile(sampleTaskShellFile));
|
||||
}
|
||||
|
||||
if (!TaskAfterFileExist) {
|
||||
fs.writeFileSync(TaskAfterFile, fs.readFileSync(sampleTaskShellFile));
|
||||
await fs.writeFile(TaskAfterFile, await fs.readFile(sampleTaskShellFile));
|
||||
}
|
||||
|
||||
dotenv.config({ path: confFile });
|
||||
|
||||
Logger.info('✌️ Init file down');
|
||||
console.log('✌️ Init file down');
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Container } from 'typedi';
|
||||
import SystemService from '../services/system';
|
||||
import ScheduleService from '../services/schedule';
|
||||
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
||||
import SubscriptionService from '../services/subscription';
|
||||
import config from '../config';
|
||||
import { fileExist } from '../config/util';
|
||||
@@ -22,7 +22,7 @@ export default async () => {
|
||||
id: NaN,
|
||||
name: '生成token',
|
||||
command: tokenCommand,
|
||||
};
|
||||
} as ScheduleTaskType;
|
||||
await scheduleService.cancelIntervalTask(cron);
|
||||
scheduleService.createIntervalTask(cron, {
|
||||
days: 28,
|
||||
@@ -30,15 +30,15 @@ export default async () => {
|
||||
|
||||
// 运行删除日志任务
|
||||
const data = await systemService.getSystemConfig();
|
||||
if (data && data.info && data.info.frequency) {
|
||||
if (data && data.info && data.info.logRemoveFrequency) {
|
||||
const rmlogCron = {
|
||||
id: data.id,
|
||||
id: data.id as number,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${data.info.frequency}`,
|
||||
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||
};
|
||||
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||
scheduleService.createIntervalTask(rmlogCron, {
|
||||
days: data.info.frequency,
|
||||
days: data.info.logRemoveFrequency,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+27
-21
@@ -1,32 +1,38 @@
|
||||
import winston from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
import config from '../config';
|
||||
import path from 'path';
|
||||
|
||||
const transports = [];
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
transports.push(new winston.transports.Console());
|
||||
} else {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.cli(),
|
||||
winston.format.splat(),
|
||||
),
|
||||
}),
|
||||
);
|
||||
const levelMap: Record<string, string> = {
|
||||
info: '🔵',
|
||||
warn: '🟡',
|
||||
error: '🔴',
|
||||
debug: '🔶'
|
||||
}
|
||||
|
||||
const customFormat = winston.format.combine(
|
||||
winston.format.splat(),
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
winston.format.align(),
|
||||
winston.format.printf((i) => `[${levelMap[i.level]}${i.level}] [${[i.timestamp]}]: ${i.message}`),
|
||||
);
|
||||
|
||||
const defaultOptions = {
|
||||
format: customFormat,
|
||||
datePattern: "YYYY-MM-DD",
|
||||
maxSize: "20m",
|
||||
maxFiles: "7d",
|
||||
};
|
||||
|
||||
const LoggerInstance = winston.createLogger({
|
||||
level: config.logs.level,
|
||||
levels: winston.config.npm.levels,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
}),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
winston.format.json(),
|
||||
),
|
||||
transports,
|
||||
transports: [
|
||||
new winston.transports.DailyRotateFile({
|
||||
filename: path.join(config.systemLogPath, '%DATE%.log'),
|
||||
...defaultOptions,
|
||||
})
|
||||
],
|
||||
});
|
||||
|
||||
export default LoggerInstance;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Application } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import * as Tracing from '@sentry/tracing';
|
||||
import Logger from './logger';
|
||||
import config from '../config';
|
||||
import fs from 'fs';
|
||||
@@ -16,10 +15,10 @@ export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
/UnauthorizedError/i,
|
||||
/celebrate request validation failed/i,
|
||||
],
|
||||
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
||||
dsn: 'https://8b5c84cfef3e22541bc84de0ed00497b@o1098464.ingest.sentry.io/6122819',
|
||||
integrations: [
|
||||
new Sentry.Integrations.Http({ tracing: true }),
|
||||
new Tracing.Integrations.Express({ app: expressApp }),
|
||||
new Sentry.Integrations.Express({ app: expressApp }),
|
||||
],
|
||||
tracesSampleRate: 0.8,
|
||||
release: version,
|
||||
@@ -29,4 +28,5 @@ export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
expressApp.use(Sentry.Handlers.tracingHandler());
|
||||
|
||||
Logger.info('✌️ Sentry loaded');
|
||||
console.log('✌️ Sentry loaded');
|
||||
};
|
||||
|
||||
+5
-11
@@ -6,17 +6,11 @@ export default async ({ server }: { server: Server }) => {
|
||||
await Sock({ server });
|
||||
Logger.info('✌️ Sock loaded');
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
Logger.info('✌️ Server need close');
|
||||
server.close(() => {
|
||||
setTimeout(() => {
|
||||
process.exit();
|
||||
}, 10000);
|
||||
});
|
||||
process.on('uncaughtException', (error) => {
|
||||
Logger.error('Uncaught exception:', error);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('Forcing server close !!!');
|
||||
process.exit(1);
|
||||
}, 15000);
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
Logger.error('Unhandled rejection:', reason, promise);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import sockJs from 'sockjs';
|
||||
import { Server } from 'http';
|
||||
import Logger from './logger';
|
||||
import { Container } from 'typedi';
|
||||
import SockService from '../services/sock';
|
||||
import config from '../config/index';
|
||||
import fs from 'fs';
|
||||
import { getPlatform } from '../config/util';
|
||||
import fs from 'fs/promises';
|
||||
import { getPlatform, safeJSONParse } from '../config/util';
|
||||
|
||||
export default async ({ server }: { server: Server }) => {
|
||||
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||
const sockService = Container.get(SockService);
|
||||
|
||||
echo.on('connection', (conn) => {
|
||||
echo.on('connection', async (conn) => {
|
||||
if (!conn.headers || !conn.url || !conn.pathname) {
|
||||
conn.close('404');
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
const data = await fs.readFile(config.authConfigFile, 'utf8');
|
||||
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||
if (data) {
|
||||
const { token = '', tokens = {} } = JSON.parse(data);
|
||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||
if (headerToken === token || tokens[platform] === headerToken) {
|
||||
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
||||
sockService.addClient(conn);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import bodyParser from 'body-parser';
|
||||
import { errors } from 'celebrate';
|
||||
import cors from 'cors';
|
||||
import { Application, NextFunction, Request, Response } from 'express';
|
||||
import jwt from 'express-jwt';
|
||||
import Container from 'typedi';
|
||||
import config from '../config';
|
||||
import SystemService from '../services/system';
|
||||
import Logger from './logger';
|
||||
|
||||
export default ({ app }: { app: Application }) => {
|
||||
app.set('trust proxy', 'loopback');
|
||||
app.use(cors());
|
||||
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||
|
||||
app.use(
|
||||
jwt({
|
||||
secret: config.secret,
|
||||
algorithms: ['HS384'],
|
||||
}),
|
||||
);
|
||||
|
||||
app.put(
|
||||
'/api/reload',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.reloadSystem();
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
Logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.put(
|
||||
'/api/system',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.reloadSystem('system');
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
Logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.put(
|
||||
'/api/data',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.reloadSystem('data');
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
Logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const err: any = new Error('Not Found');
|
||||
err['status'] = 404;
|
||||
next(err);
|
||||
});
|
||||
|
||||
app.use(errors());
|
||||
|
||||
app.use(
|
||||
(
|
||||
err: Error & { status: number },
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
if (err.name === 'UnauthorizedError') {
|
||||
return res
|
||||
.status(err.status)
|
||||
.send({ code: 401, message: err.message })
|
||||
.end();
|
||||
}
|
||||
return next(err);
|
||||
},
|
||||
);
|
||||
|
||||
app.use(
|
||||
(
|
||||
err: Error & { status: number },
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
code: err.status || 500,
|
||||
message: err.message,
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -7,10 +7,14 @@ service Cron {
|
||||
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; }
|
||||
|
||||
+158
-138
@@ -10,78 +10,161 @@ import {
|
||||
Metadata,
|
||||
ServiceError,
|
||||
UntypedServiceImplementation,
|
||||
} from '@grpc/grpc-js';
|
||||
import _m0 from 'protobufjs/minimal';
|
||||
} from "@grpc/grpc-js";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = 'com.ql.cron';
|
||||
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 AddCronResponse {
|
||||
}
|
||||
|
||||
export interface DeleteCronRequest {
|
||||
ids: string[];
|
||||
}
|
||||
|
||||
export interface DeleteCronResponse {}
|
||||
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: '' };
|
||||
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
|
||||
}
|
||||
|
||||
export const ICron = {
|
||||
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
if (message.id !== '') {
|
||||
if (message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
if (message.schedule !== '') {
|
||||
if (message.schedule !== "") {
|
||||
writer.uint32(18).string(message.schedule);
|
||||
}
|
||||
if (message.command !== '') {
|
||||
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);
|
||||
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) {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
case 2:
|
||||
if (tag != 18) {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.schedule = reader.string();
|
||||
continue;
|
||||
case 3:
|
||||
if (tag != 26) {
|
||||
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) {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
@@ -91,9 +174,13 @@ export const ICron = {
|
||||
|
||||
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) : '',
|
||||
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) : "",
|
||||
};
|
||||
},
|
||||
|
||||
@@ -102,6 +189,12 @@ export const ICron = {
|
||||
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;
|
||||
},
|
||||
|
||||
@@ -111,9 +204,11 @@ export const ICron = {
|
||||
|
||||
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.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;
|
||||
},
|
||||
};
|
||||
@@ -123,10 +218,7 @@ function createBaseAddCronRequest(): AddCronRequest {
|
||||
}
|
||||
|
||||
export const AddCronRequest = {
|
||||
encode(
|
||||
message: AddCronRequest,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
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();
|
||||
}
|
||||
@@ -134,22 +226,21 @@ export const AddCronRequest = {
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
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) {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.crons.push(ICron.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
@@ -158,32 +249,24 @@ export const AddCronRequest = {
|
||||
},
|
||||
|
||||
fromJSON(object: any): AddCronRequest {
|
||||
return {
|
||||
crons: Array.isArray(object?.crons)
|
||||
? object.crons.map((e: any) => ICron.fromJSON(e))
|
||||
: [],
|
||||
};
|
||||
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));
|
||||
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 {
|
||||
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
|
||||
return AddCronRequest.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(
|
||||
object: I,
|
||||
): AddCronRequest {
|
||||
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
|
||||
const message = createBaseAddCronRequest();
|
||||
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
|
||||
return message;
|
||||
@@ -195,23 +278,19 @@ function createBaseAddCronResponse(): AddCronResponse {
|
||||
}
|
||||
|
||||
export const AddCronResponse = {
|
||||
encode(
|
||||
_: AddCronResponse,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
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);
|
||||
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) {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
@@ -228,15 +307,11 @@ export const AddCronResponse = {
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(
|
||||
base?: I,
|
||||
): AddCronResponse {
|
||||
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
|
||||
return AddCronResponse.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(
|
||||
_: I,
|
||||
): AddCronResponse {
|
||||
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
|
||||
const message = createBaseAddCronResponse();
|
||||
return message;
|
||||
},
|
||||
@@ -247,10 +322,7 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
|
||||
}
|
||||
|
||||
export const DeleteCronRequest = {
|
||||
encode(
|
||||
message: DeleteCronRequest,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
for (const v of message.ids) {
|
||||
writer.uint32(10).string(v!);
|
||||
}
|
||||
@@ -258,22 +330,21 @@ export const DeleteCronRequest = {
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
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) {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.ids.push(reader.string());
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
@@ -282,11 +353,7 @@ export const DeleteCronRequest = {
|
||||
},
|
||||
|
||||
fromJSON(object: any): DeleteCronRequest {
|
||||
return {
|
||||
ids: Array.isArray(object?.ids)
|
||||
? object.ids.map((e: any) => String(e))
|
||||
: [],
|
||||
};
|
||||
return { ids: Array.isArray(object?.ids) ? object.ids.map((e: any) => String(e)) : [] };
|
||||
},
|
||||
|
||||
toJSON(message: DeleteCronRequest): unknown {
|
||||
@@ -299,15 +366,11 @@ export const DeleteCronRequest = {
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
|
||||
base?: I,
|
||||
): DeleteCronRequest {
|
||||
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
|
||||
return DeleteCronRequest.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
|
||||
object: I,
|
||||
): DeleteCronRequest {
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
|
||||
const message = createBaseDeleteCronRequest();
|
||||
message.ids = object.ids?.map((e) => e) || [];
|
||||
return message;
|
||||
@@ -319,23 +382,19 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
|
||||
}
|
||||
|
||||
export const DeleteCronResponse = {
|
||||
encode(
|
||||
_: DeleteCronResponse,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
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);
|
||||
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) {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
@@ -352,15 +411,11 @@ export const DeleteCronResponse = {
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
|
||||
base?: I,
|
||||
): DeleteCronResponse {
|
||||
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
|
||||
return DeleteCronResponse.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
|
||||
_: I,
|
||||
): DeleteCronResponse {
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
|
||||
const message = createBaseDeleteCronResponse();
|
||||
return message;
|
||||
},
|
||||
@@ -369,25 +424,21 @@ export const DeleteCronResponse = {
|
||||
export type CronService = typeof CronService;
|
||||
export const CronService = {
|
||||
addCron: {
|
||||
path: '/com.ql.cron.Cron/addCron',
|
||||
path: "/com.ql.cron.Cron/addCron",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: AddCronRequest) =>
|
||||
Buffer.from(AddCronRequest.encode(value).finish()),
|
||||
requestSerialize: (value: AddCronRequest) => Buffer.from(AddCronRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
|
||||
responseSerialize: (value: AddCronResponse) =>
|
||||
Buffer.from(AddCronResponse.encode(value).finish()),
|
||||
responseSerialize: (value: AddCronResponse) => Buffer.from(AddCronResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
|
||||
},
|
||||
delCron: {
|
||||
path: '/com.ql.cron.Cron/delCron',
|
||||
path: "/com.ql.cron.Cron/delCron",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: DeleteCronRequest) =>
|
||||
Buffer.from(DeleteCronRequest.encode(value).finish()),
|
||||
requestSerialize: (value: DeleteCronRequest) => Buffer.from(DeleteCronRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
|
||||
responseSerialize: (value: DeleteCronResponse) =>
|
||||
Buffer.from(DeleteCronResponse.encode(value).finish()),
|
||||
responseSerialize: (value: DeleteCronResponse) => Buffer.from(DeleteCronResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
|
||||
},
|
||||
} as const;
|
||||
@@ -415,67 +466,36 @@ export interface CronClient extends Client {
|
||||
): ClientUnaryCall;
|
||||
delCron(
|
||||
request: DeleteCronRequest,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: DeleteCronResponse,
|
||||
) => void,
|
||||
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
delCron(
|
||||
request: DeleteCronRequest,
|
||||
metadata: Metadata,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: DeleteCronResponse,
|
||||
) => void,
|
||||
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
delCron(
|
||||
request: DeleteCronRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: DeleteCronResponse,
|
||||
) => void,
|
||||
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;
|
||||
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;
|
||||
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]> }
|
||||
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;
|
||||
};
|
||||
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;
|
||||
|
||||
+51
-114
@@ -12,10 +12,10 @@ import {
|
||||
Metadata,
|
||||
ServiceError,
|
||||
UntypedServiceImplementation,
|
||||
} from '@grpc/grpc-js';
|
||||
import _m0 from 'protobufjs/minimal';
|
||||
} from "@grpc/grpc-js";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = 'com.ql.health';
|
||||
export const protobufPackage = "com.ql.health";
|
||||
|
||||
export interface HealthCheckRequest {
|
||||
service: string;
|
||||
@@ -33,79 +33,71 @@ export enum HealthCheckResponse_ServingStatus {
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function healthCheckResponse_ServingStatusFromJSON(
|
||||
object: any,
|
||||
): HealthCheckResponse_ServingStatus {
|
||||
export function healthCheckResponse_ServingStatusFromJSON(object: any): HealthCheckResponse_ServingStatus {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case 'UNKNOWN':
|
||||
case "UNKNOWN":
|
||||
return HealthCheckResponse_ServingStatus.UNKNOWN;
|
||||
case 1:
|
||||
case 'SERVING':
|
||||
case "SERVING":
|
||||
return HealthCheckResponse_ServingStatus.SERVING;
|
||||
case 2:
|
||||
case 'NOT_SERVING':
|
||||
case "NOT_SERVING":
|
||||
return HealthCheckResponse_ServingStatus.NOT_SERVING;
|
||||
case 3:
|
||||
case 'SERVICE_UNKNOWN':
|
||||
case "SERVICE_UNKNOWN":
|
||||
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
|
||||
case -1:
|
||||
case 'UNRECOGNIZED':
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function healthCheckResponse_ServingStatusToJSON(
|
||||
object: HealthCheckResponse_ServingStatus,
|
||||
): string {
|
||||
export function healthCheckResponse_ServingStatusToJSON(object: HealthCheckResponse_ServingStatus): string {
|
||||
switch (object) {
|
||||
case HealthCheckResponse_ServingStatus.UNKNOWN:
|
||||
return 'UNKNOWN';
|
||||
return "UNKNOWN";
|
||||
case HealthCheckResponse_ServingStatus.SERVING:
|
||||
return 'SERVING';
|
||||
return "SERVING";
|
||||
case HealthCheckResponse_ServingStatus.NOT_SERVING:
|
||||
return 'NOT_SERVING';
|
||||
return "NOT_SERVING";
|
||||
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
|
||||
return 'SERVICE_UNKNOWN';
|
||||
return "SERVICE_UNKNOWN";
|
||||
case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
|
||||
default:
|
||||
return 'UNRECOGNIZED';
|
||||
return "UNRECOGNIZED";
|
||||
}
|
||||
}
|
||||
|
||||
function createBaseHealthCheckRequest(): HealthCheckRequest {
|
||||
return { service: '' };
|
||||
return { service: "" };
|
||||
}
|
||||
|
||||
export const HealthCheckRequest = {
|
||||
encode(
|
||||
message: HealthCheckRequest,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
if (message.service !== '') {
|
||||
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);
|
||||
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) {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.service = reader.string();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
@@ -114,7 +106,7 @@ export const HealthCheckRequest = {
|
||||
},
|
||||
|
||||
fromJSON(object: any): HealthCheckRequest {
|
||||
return { service: isSet(object.service) ? String(object.service) : '' };
|
||||
return { service: isSet(object.service) ? String(object.service) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: HealthCheckRequest): unknown {
|
||||
@@ -123,17 +115,13 @@ export const HealthCheckRequest = {
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
|
||||
base?: I,
|
||||
): HealthCheckRequest {
|
||||
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
|
||||
return HealthCheckRequest.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
|
||||
object: I,
|
||||
): HealthCheckRequest {
|
||||
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
|
||||
const message = createBaseHealthCheckRequest();
|
||||
message.service = object.service ?? '';
|
||||
message.service = object.service ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
@@ -143,10 +131,7 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
|
||||
}
|
||||
|
||||
export const HealthCheckResponse = {
|
||||
encode(
|
||||
message: HealthCheckResponse,
|
||||
writer: _m0.Writer = _m0.Writer.create(),
|
||||
): _m0.Writer {
|
||||
encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
if (message.status !== 0) {
|
||||
writer.uint32(8).int32(message.status);
|
||||
}
|
||||
@@ -154,22 +139,21 @@ export const HealthCheckResponse = {
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
|
||||
const reader =
|
||||
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
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) {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.status = reader.int32() as any;
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) == 4 || tag == 0) {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
@@ -178,29 +162,20 @@ export const HealthCheckResponse = {
|
||||
},
|
||||
|
||||
fromJSON(object: any): HealthCheckResponse {
|
||||
return {
|
||||
status: isSet(object.status)
|
||||
? healthCheckResponse_ServingStatusFromJSON(object.status)
|
||||
: 0,
|
||||
};
|
||||
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));
|
||||
message.status !== undefined && (obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
|
||||
base?: I,
|
||||
): HealthCheckResponse {
|
||||
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
|
||||
return HealthCheckResponse.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
|
||||
object: I,
|
||||
): HealthCheckResponse {
|
||||
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
|
||||
const message = createBaseHealthCheckResponse();
|
||||
message.status = object.status ?? 0;
|
||||
return message;
|
||||
@@ -210,25 +185,21 @@ export const HealthCheckResponse = {
|
||||
export type HealthService = typeof HealthService;
|
||||
export const HealthService = {
|
||||
check: {
|
||||
path: '/com.ql.health.Health/Check',
|
||||
path: "/com.ql.health.Health/Check",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: HealthCheckRequest) =>
|
||||
Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
||||
responseSerialize: (value: HealthCheckResponse) =>
|
||||
Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
||||
},
|
||||
watch: {
|
||||
path: '/com.ql.health.Health/Watch',
|
||||
path: "/com.ql.health.Health/Watch",
|
||||
requestStream: false,
|
||||
responseStream: true,
|
||||
requestSerialize: (value: HealthCheckRequest) =>
|
||||
Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
||||
responseSerialize: (value: HealthCheckResponse) =>
|
||||
Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
||||
},
|
||||
} as const;
|
||||
@@ -241,32 +212,20 @@ export interface HealthServer extends UntypedServiceImplementation {
|
||||
export interface HealthClient extends Client {
|
||||
check(
|
||||
request: HealthCheckRequest,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: HealthCheckResponse,
|
||||
) => void,
|
||||
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
check(
|
||||
request: HealthCheckRequest,
|
||||
metadata: Metadata,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: HealthCheckResponse,
|
||||
) => void,
|
||||
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
check(
|
||||
request: HealthCheckRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: HealthCheckResponse,
|
||||
) => void,
|
||||
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
watch(
|
||||
request: HealthCheckRequest,
|
||||
options?: Partial<CallOptions>,
|
||||
): ClientReadableStream<HealthCheckResponse>;
|
||||
watch(request: HealthCheckRequest, options?: Partial<CallOptions>): ClientReadableStream<HealthCheckResponse>;
|
||||
watch(
|
||||
request: HealthCheckRequest,
|
||||
metadata?: Metadata,
|
||||
@@ -274,43 +233,21 @@ export interface HealthClient extends Client {
|
||||
): ClientReadableStream<HealthCheckResponse>;
|
||||
}
|
||||
|
||||
export const HealthClient = makeGenericClientConstructor(
|
||||
HealthService,
|
||||
'com.ql.health.Health',
|
||||
) as unknown as {
|
||||
new (
|
||||
address: string,
|
||||
credentials: ChannelCredentials,
|
||||
options?: Partial<ClientOptions>,
|
||||
): HealthClient;
|
||||
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;
|
||||
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]> }
|
||||
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;
|
||||
};
|
||||
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;
|
||||
|
||||
+6
-3
@@ -6,28 +6,31 @@ import { credentials } from '@grpc/grpc-js';
|
||||
|
||||
const app = express();
|
||||
const client = new HealthClient(
|
||||
`localhost:${config.cronPort}`,
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
credentials.createInsecure(),
|
||||
{ 'grpc.enable_http_proxy': 0 },
|
||||
);
|
||||
|
||||
app.get('/api/health', (req, res) => {
|
||||
client.check({ service: 'cron' }, (err, response) => {
|
||||
if (err) {
|
||||
return res.status(500).send({ code: 500, error: err });
|
||||
return res.status(200).send({ code: 500, error: err });
|
||||
}
|
||||
return res.status(200).send({ code: 200, data: response });
|
||||
});
|
||||
});
|
||||
|
||||
app
|
||||
.listen(config.publicPort, async () => {
|
||||
.listen(config.publicPort, '0.0.0.0', async () => {
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
await require('./loaders/db').default();
|
||||
|
||||
Logger.debug(`✌️ 公共服务启动成功!`);
|
||||
console.debug(`✌️ 公共服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -3,22 +3,52 @@ 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 } = item;
|
||||
const { id, schedule, command, extraSchedules, name } = item;
|
||||
if (scheduleStacks.has(id)) {
|
||||
scheduleStacks.get(id)?.cancel();
|
||||
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
||||
}
|
||||
scheduleStacks.set(
|
||||
|
||||
Logger.info(
|
||||
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||
id,
|
||||
nodeSchedule.scheduleJob(id, schedule, async () => {
|
||||
runCron(`ID=${id} ${command}`)
|
||||
}),
|
||||
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);
|
||||
|
||||
@@ -10,8 +10,9 @@ import config from '../config';
|
||||
|
||||
class Client {
|
||||
private client = new CronClient(
|
||||
`localhost:${config.cronPort}`,
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
credentials.createInsecure(),
|
||||
{ 'grpc.enable_http_proxy': 0 },
|
||||
);
|
||||
|
||||
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import { ToadScheduler } from 'toad-scheduler';
|
||||
|
||||
export const scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||
export const scheduleStacks = new Map<string, nodeSchedule.Job[]>();
|
||||
|
||||
export const intervalSchedule = new ToadScheduler();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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>,
|
||||
@@ -8,7 +9,11 @@ const delCron = (
|
||||
) => {
|
||||
for (const id of call.request.ids) {
|
||||
if (scheduleStacks.has(id)) {
|
||||
scheduleStacks.get(id)?.cancel();
|
||||
Logger.info(
|
||||
'[schedule][取消定时任务], 任务ID: %s',
|
||||
id,
|
||||
);
|
||||
scheduleStacks.get(id)?.forEach(x => x.cancel());
|
||||
scheduleStacks.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const check = async (
|
||||
switch (call.request.service) {
|
||||
case 'cron':
|
||||
const res = await promiseExec(
|
||||
`curl -s http://localhost:${config.port}/api/system`,
|
||||
`curl -s --noproxy '*' http://0.0.0.0:${config.port}/api/system`,
|
||||
);
|
||||
|
||||
if (res.includes('200')) {
|
||||
@@ -24,7 +24,7 @@ const check = async (
|
||||
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
|
||||
);
|
||||
return callback(
|
||||
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`),
|
||||
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`.trim()),
|
||||
);
|
||||
|
||||
default:
|
||||
|
||||
@@ -7,11 +7,11 @@ import { check } from './health';
|
||||
import config from '../config';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
const server = new Server();
|
||||
const server = new Server({ 'grpc.enable_http_proxy': 0 });
|
||||
server.addService(HealthService, { check });
|
||||
server.addService(CronService, { addCron, delCron });
|
||||
server.bindAsync(
|
||||
`localhost:${config.cronPort}`,
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
ServerCredentials.createInsecure(),
|
||||
(err, port) => {
|
||||
if (err) {
|
||||
@@ -19,6 +19,7 @@ server.bindAsync(
|
||||
}
|
||||
server.start();
|
||||
Logger.debug(`✌️ 定时服务启动成功!`);
|
||||
console.debug(`✌️ 定时服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import path, { join } from 'path';
|
||||
import config from '../config';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
import { Response } from 'express';
|
||||
import got from 'got';
|
||||
|
||||
@Service()
|
||||
export default class ConfigService {
|
||||
constructor() {}
|
||||
|
||||
public async getFile(filePath: string, res: Response) {
|
||||
let content = '';
|
||||
if (config.blackFileList.includes(filePath) || !filePath) {
|
||||
res.send({ code: 403, message: '文件无法访问' });
|
||||
}
|
||||
if (filePath.startsWith('sample/')) {
|
||||
const res = await got.get(
|
||||
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
|
||||
);
|
||||
content = res.body;
|
||||
} else if (filePath.startsWith('data/scripts/')) {
|
||||
content = await getFileContentByName(join(config.rootPath, filePath));
|
||||
} else {
|
||||
content = await getFileContentByName(join(config.configPath, filePath));
|
||||
}
|
||||
|
||||
res.send({ code: 200, data: content });
|
||||
}
|
||||
}
|
||||
+136
-87
@@ -3,28 +3,32 @@ import winston from 'winston';
|
||||
import config from '../config';
|
||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import cron_parser from 'cron-parser';
|
||||
import {
|
||||
getFileContentByName,
|
||||
fileExist,
|
||||
killTask,
|
||||
getUniqPath,
|
||||
safeJSONParse,
|
||||
} from '../config/util';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
|
||||
import path from 'path';
|
||||
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
||||
import cronClient from '../schedule/client';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import { spawn } from 'cross-spawn';
|
||||
import dayjs from 'dayjs';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
private isSixCron(cron: Crontab) {
|
||||
const { schedule } = cron;
|
||||
if (schedule?.split(/ +/).length === 6) {
|
||||
private isNodeCron(cron: Crontab) {
|
||||
const { schedule, extra_schedules } = cron;
|
||||
if (Number(schedule?.split(/ +/).length) > 5 || extra_schedules?.length) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -34,9 +38,15 @@ export default class CronService {
|
||||
const tab = new Crontab(payload);
|
||||
tab.saved = false;
|
||||
const doc = await this.insert(tab);
|
||||
if (this.isSixCron(doc)) {
|
||||
if (this.isNodeCron(doc)) {
|
||||
await cronClient.addCron([
|
||||
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command },
|
||||
{
|
||||
name: doc.name || '',
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: this.makeCommand(doc),
|
||||
extraSchedules: doc.extra_schedules || [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
await this.set_crontab();
|
||||
@@ -55,15 +65,17 @@ export default class CronService {
|
||||
if (doc.isDisabled === 1) {
|
||||
return newDoc;
|
||||
}
|
||||
if (this.isSixCron(doc)) {
|
||||
await cronClient.delCron([String(newDoc.id)]);
|
||||
if (this.isNodeCron(doc)) {
|
||||
await cronClient.delCron([String(doc.id)]);
|
||||
}
|
||||
if (this.isSixCron(newDoc)) {
|
||||
if (this.isNodeCron(newDoc)) {
|
||||
await cronClient.addCron([
|
||||
{
|
||||
name: doc.name || '',
|
||||
id: String(newDoc.id),
|
||||
schedule: newDoc.schedule!,
|
||||
command: newDoc.command,
|
||||
command: this.makeCommand(newDoc),
|
||||
extraSchedules: newDoc.extra_schedules || [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -91,7 +103,7 @@ export default class CronService {
|
||||
last_running_time: number;
|
||||
last_execution_time: number;
|
||||
}) {
|
||||
const options: any = {
|
||||
let options: any = {
|
||||
status,
|
||||
pid,
|
||||
log_path,
|
||||
@@ -101,7 +113,22 @@ export default class CronService {
|
||||
options.last_running_time = last_running_time;
|
||||
}
|
||||
|
||||
return await CrontabModel.update({ ...options }, { where: { id: ids } });
|
||||
for (const id of ids) {
|
||||
let cron;
|
||||
try {
|
||||
cron = await this.getDb({ id });
|
||||
} catch (err) {}
|
||||
if (!cron) {
|
||||
continue;
|
||||
}
|
||||
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[]) {
|
||||
@@ -165,7 +192,7 @@ export default class CronService {
|
||||
case 'In':
|
||||
q[Op.or] = [
|
||||
{
|
||||
[property]: value,
|
||||
[property]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
property === 'status' && value.includes(2)
|
||||
? { isDisabled: 1 }
|
||||
@@ -176,7 +203,7 @@ export default class CronService {
|
||||
q[Op.and] = [
|
||||
{
|
||||
[property]: {
|
||||
[Op.notIn]: value,
|
||||
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
},
|
||||
property === 'status' && value.includes(2)
|
||||
@@ -312,9 +339,9 @@ export default class CronService {
|
||||
const searchText = params?.searchValue;
|
||||
const page = Number(params?.page || '0');
|
||||
const size = Number(params?.size || '0');
|
||||
const viewQuery = JSON.parse(params?.queryString || '{}');
|
||||
const filterQuery = JSON.parse(params?.filters || '{}');
|
||||
const sorterQuery = JSON.parse(params?.sorter || '{}');
|
||||
const viewQuery = safeJSONParse(params?.queryString);
|
||||
const filterQuery = safeJSONParse(params?.filters);
|
||||
const sorterQuery = safeJSONParse(params?.sorter);
|
||||
|
||||
let query: any = {};
|
||||
let order = [
|
||||
@@ -354,7 +381,10 @@ export default class CronService {
|
||||
|
||||
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {
|
||||
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Crontab);
|
||||
if (!doc) {
|
||||
throw new Error(`Cron ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async run(ids: number[]) {
|
||||
@@ -374,7 +404,7 @@ export default class CronService {
|
||||
try {
|
||||
await killTask(doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.silly(error);
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,65 +415,62 @@ export default class CronService {
|
||||
);
|
||||
}
|
||||
|
||||
private async runSingle(cronId: number): Promise<number> {
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
private async runSingle(cronId: number): Promise<number | void> {
|
||||
return taskLimit.manualRunWithCronLimit(() => {
|
||||
return new Promise(async (resolve: any) => {
|
||||
const cron = await this.getDb({ id: cronId });
|
||||
const params = {
|
||||
name: cron.name,
|
||||
command: cron.command,
|
||||
schedule: cron.schedule,
|
||||
extraSchedules: cron.extra_schedules,
|
||||
};
|
||||
if (cron.status !== CrontabStatus.queued) {
|
||||
resolve();
|
||||
resolve(params);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`,
|
||||
);
|
||||
|
||||
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}`;
|
||||
const uniqPath = await getUniqPath(command, `${id}`);
|
||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
|
||||
await fs.mkdir(logDirPath, { recursive: true });
|
||||
}
|
||||
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' });
|
||||
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,
|
||||
true,
|
||||
)}`,
|
||||
{ shell: '/bin/bash' },
|
||||
);
|
||||
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.running, pid: cp.pid },
|
||||
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
|
||||
{ where: { id } },
|
||||
);
|
||||
cp.stderr.on('data', (data) => {
|
||||
if (logFileExist) {
|
||||
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
|
||||
}
|
||||
cp.stdout.on('data', async (data) => {
|
||||
await fs.appendFile(absolutePath, data.toString());
|
||||
});
|
||||
cp.on('error', (err) => {
|
||||
if (logFileExist) {
|
||||
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||
}
|
||||
cp.stderr.on('data', async (data) => {
|
||||
await fs.appendFile(absolutePath, data.toString());
|
||||
});
|
||||
cp.on('error', async (err) => {
|
||||
await fs.appendFile(absolutePath, JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
||||
);
|
||||
});
|
||||
cp.on('close', async (code) => {
|
||||
cp.on('exit', async (code) => {
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
resolve();
|
||||
resolve({ ...params, pid: cp.pid, code });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -459,11 +486,13 @@ export default class CronService {
|
||||
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
const sixCron = docs
|
||||
.filter((x) => this.isSixCron(x))
|
||||
.filter((x) => this.isNodeCron(x))
|
||||
.map((doc) => ({
|
||||
name: doc.name || '',
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: doc.command,
|
||||
command: this.makeCommand(doc),
|
||||
extraSchedules: doc.extra_schedules || [],
|
||||
}));
|
||||
await cronClient.addCron(sixCron);
|
||||
await this.set_crontab();
|
||||
@@ -478,7 +507,7 @@ export default class CronService {
|
||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||
if (logFileExist) {
|
||||
return getFileContentByName(`${absolutePath}`);
|
||||
return await getFileContentByName(`${absolutePath}`);
|
||||
} else {
|
||||
return '任务未运行';
|
||||
}
|
||||
@@ -492,28 +521,43 @@ export default class CronService {
|
||||
|
||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||
const dir = path.resolve(config.logPath, relativeDir);
|
||||
if (existsSync(dir)) {
|
||||
let files = await promises.readdir(dir);
|
||||
return files
|
||||
.map((x) => ({
|
||||
filename: x,
|
||||
directory: relativeDir.replace(config.logPath, ''),
|
||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
||||
}))
|
||||
.sort((a, b) => b.time - a.time);
|
||||
const dirExist = await fileExist(dir);
|
||||
if (dirExist) {
|
||||
let files = await fs.readdir(dir);
|
||||
return (
|
||||
await Promise.all(
|
||||
files.map(async (x) => ({
|
||||
filename: x,
|
||||
directory: relativeDir.replace(config.logPath, ''),
|
||||
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(),
|
||||
})),
|
||||
)
|
||||
).sort((a, b) => b.time - a.time);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private make_command(tab: Crontab) {
|
||||
if (
|
||||
!tab.command.startsWith(TASK_PREFIX) &&
|
||||
!tab.command.startsWith(QL_PREFIX)
|
||||
) {
|
||||
tab.command = `${TASK_PREFIX}${tab.command}`;
|
||||
private makeCommand(tab: Crontab, realTime?: boolean) {
|
||||
let command = tab.command.trim();
|
||||
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||
command = `${TASK_PREFIX}${tab.command}`;
|
||||
}
|
||||
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
||||
let commandVariable = `real_time=${Boolean(realTime)} 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;
|
||||
}
|
||||
|
||||
@@ -522,22 +566,25 @@ export default class CronService {
|
||||
var crontab_string = '';
|
||||
tabs.data.forEach((tab) => {
|
||||
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 += tab.schedule;
|
||||
crontab_string += ' ';
|
||||
crontab_string += this.make_command(tab);
|
||||
crontab_string += this.makeCommand(tab);
|
||||
crontab_string += '\n';
|
||||
} else {
|
||||
crontab_string += tab.schedule;
|
||||
crontab_string += ' ';
|
||||
crontab_string += this.make_command(tab);
|
||||
crontab_string += this.makeCommand(tab);
|
||||
crontab_string += '\n';
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.silly(crontab_string);
|
||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||
await fs.writeFile(config.crontabFile, crontab_string);
|
||||
|
||||
execSync(`crontab ${config.crontabFile}`);
|
||||
await CrontabModel.update({ saved: true }, { where: {} });
|
||||
@@ -582,11 +629,13 @@ export default class CronService {
|
||||
this.set_crontab(tabs);
|
||||
|
||||
const sixCron = tabs.data
|
||||
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
|
||||
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
|
||||
.map((doc) => ({
|
||||
name: doc.name || '',
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: doc.command,
|
||||
command: this.makeCommand(doc),
|
||||
extraSchedules: doc.extra_schedules || [],
|
||||
}));
|
||||
await cronClient.addCron(sixCron);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class CronViewService {
|
||||
}
|
||||
|
||||
public async update(payload: CrontabView): Promise<CrontabView> {
|
||||
const doc = await this.getDb({ id: payload.id })
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new CrontabView({ ...doc, ...payload });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
return newDoc;
|
||||
@@ -59,9 +59,14 @@ export default class CronViewService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getDb(query: FindOptions<CrontabView>['where']): Promise<CrontabView> {
|
||||
public async getDb(
|
||||
query: FindOptions<CrontabView>['where'],
|
||||
): Promise<CrontabView> {
|
||||
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as CrontabView);
|
||||
if (!doc) {
|
||||
throw new Error(`CronView ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
|
||||
+170
-30
@@ -8,11 +8,18 @@ import {
|
||||
DependenceTypes,
|
||||
unInstallDependenceCommandTypes,
|
||||
DependenceModel,
|
||||
GetDependenceCommandTypes,
|
||||
versionDependenceCommandTypes,
|
||||
} from '../data/dependence';
|
||||
import { spawn } from 'cross-spawn';
|
||||
import SockService from './sock';
|
||||
import { FindOptions, Op } from 'sequelize';
|
||||
import { concurrentRun } from '../config/util';
|
||||
import {
|
||||
fileExist,
|
||||
getPid,
|
||||
killTask,
|
||||
promiseExecSuccess,
|
||||
} from '../config/util';
|
||||
import dayjs from 'dayjs';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
|
||||
@@ -21,7 +28,7 @@ export default class DependenceService {
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private sockService: SockService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||
const tabs = payloads.map((x) => {
|
||||
@@ -59,12 +66,23 @@ export default class DependenceService {
|
||||
}
|
||||
|
||||
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.queued, log: [] },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
this.installDependenceOneByOne(docs, false, force);
|
||||
const unInstalledDeps = docs.filter(
|
||||
(x) => x.status !== DependenceStatus.installed,
|
||||
);
|
||||
const installedDeps = docs.filter(
|
||||
(x) => x.status === DependenceStatus.installed,
|
||||
);
|
||||
await this.removeDb(unInstalledDeps.map((x) => x.id!));
|
||||
|
||||
if (installedDeps.length) {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.queued, log: [] },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
|
||||
this.installDependenceOneByOne(docs, false, force);
|
||||
}
|
||||
return docs;
|
||||
}
|
||||
|
||||
@@ -73,11 +91,21 @@ export default class DependenceService {
|
||||
}
|
||||
|
||||
public async dependencies(
|
||||
{ searchValue, type }: { searchValue: string; type: string },
|
||||
sort: any = { position: -1 },
|
||||
{
|
||||
searchValue,
|
||||
type,
|
||||
status,
|
||||
}: { searchValue: string; type: string; status: string },
|
||||
sort: any = [],
|
||||
query: any = {},
|
||||
): Promise<Dependence[]> {
|
||||
let condition = { ...query, type: DependenceTypes[type as any] };
|
||||
let condition = {
|
||||
...query,
|
||||
type: DependenceTypes[type as any],
|
||||
};
|
||||
if (status) {
|
||||
condition.status = status.split(',').map(Number);
|
||||
}
|
||||
if (searchValue) {
|
||||
const encodeText = encodeURI(searchValue);
|
||||
const reg = {
|
||||
@@ -93,7 +121,7 @@ export default class DependenceService {
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await this.find(condition);
|
||||
const result = await this.find(condition, sort);
|
||||
return result as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -117,10 +145,32 @@ export default class DependenceService {
|
||||
);
|
||||
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
this.installDependenceOneByOne(docs);
|
||||
this.installDependenceOneByOne(docs, true, true);
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async cancel(ids: number[]) {
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
taskLimit.removeQueuedDependency(doc);
|
||||
const depInstallCommand = InstallDependenceCommandTypes[doc.type];
|
||||
const depUnInstallCommand = unInstallDependenceCommandTypes[doc.type];
|
||||
const installCmd = `${depInstallCommand} ${doc.name.trim()}`;
|
||||
const unInstallCmd = `${depUnInstallCommand} ${doc.name.trim()}`;
|
||||
const pids = await Promise.all([
|
||||
getPid(installCmd),
|
||||
getPid(unInstallCmd),
|
||||
]);
|
||||
for (const pid of pids) {
|
||||
pid && (await killTask(pid));
|
||||
}
|
||||
}
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.cancelled },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
}
|
||||
|
||||
private async find(query: any, sort: any = []): Promise<Dependence[]> {
|
||||
const docs = await DependenceModel.findAll({
|
||||
where: { ...query },
|
||||
@@ -133,13 +183,24 @@ export default class DependenceService {
|
||||
query: FindOptions<Dependence>['where'],
|
||||
): Promise<Dependence> {
|
||||
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Dependence);
|
||||
if (!doc) {
|
||||
throw new Error(`Dependency ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
private async updateLog(ids: number[], log: string): Promise<void> {
|
||||
const doc = await DependenceModel.findOne({ where: { id: ids } });
|
||||
const newLog = doc?.log ? [...doc.log, log] : [log];
|
||||
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
|
||||
taskLimit.updateDepLog(async () => {
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
const newLog = doc?.log ? [...doc.log, log] : [log];
|
||||
await DependenceModel.update(
|
||||
{ log: newLog },
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public installOrUninstallDependency(
|
||||
@@ -147,23 +208,29 @@ export default class DependenceService {
|
||||
isInstall: boolean = true,
|
||||
force: boolean = false,
|
||||
) {
|
||||
return taskLimit.runOneByOne(() => {
|
||||
return taskLimit.runDependeny(dependency, () => {
|
||||
return new Promise(async (resolve) => {
|
||||
if (taskLimit.firstDependencyId !== dependency.id) {
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
taskLimit.removeQueuedDependency(dependency);
|
||||
|
||||
const depIds = [dependency.id!];
|
||||
const status = isInstall
|
||||
? DependenceStatus.installing
|
||||
: DependenceStatus.removing;
|
||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||
|
||||
const socketMessageType = !force
|
||||
const socketMessageType = isInstall
|
||||
? 'installDependence'
|
||||
: 'uninstallDependence';
|
||||
const depName = dependency.name;
|
||||
let depName = dependency.name.trim();
|
||||
const depRunCommand = (
|
||||
isInstall
|
||||
? InstallDependenceCommandTypes
|
||||
: unInstallDependenceCommandTypes
|
||||
)[dependency.type as any];
|
||||
)[dependency.type];
|
||||
const actionText = isInstall ? '安装' : '删除';
|
||||
const startTime = dayjs();
|
||||
|
||||
@@ -175,11 +242,74 @@ export default class DependenceService {
|
||||
message,
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, message);
|
||||
this.updateLog(depIds, message);
|
||||
|
||||
const cp = spawn(`${depRunCommand} ${depName}`, {
|
||||
shell: '/bin/bash',
|
||||
});
|
||||
// 判断是否已经安装过依赖
|
||||
if (isInstall && !force) {
|
||||
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
|
||||
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
||||
let depVersion = '';
|
||||
if (depName.includes(depVersionStr)) {
|
||||
const symbolRegx = new RegExp(
|
||||
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
|
||||
);
|
||||
const [, _depName, _depVersion] = depName.match(symbolRegx) || [];
|
||||
if (_depVersion && _depName) {
|
||||
depName = _depName;
|
||||
depVersion = _depVersion;
|
||||
}
|
||||
}
|
||||
const isNodeDependence = dependency.type === DependenceTypes.nodejs;
|
||||
const isLinuxDependence = dependency.type === DependenceTypes.linux;
|
||||
const isPythonDependence =
|
||||
dependency.type === DependenceTypes.python3;
|
||||
const depInfo = (
|
||||
await promiseExecSuccess(
|
||||
isNodeDependence
|
||||
? `${getCommandPrefix} | grep "${depName}" | head -1`
|
||||
: `${getCommandPrefix} ${depName}`,
|
||||
)
|
||||
)
|
||||
.replace(/\s{2,}/, ' ')
|
||||
.replace(/\s+$/, '');
|
||||
|
||||
if (
|
||||
depInfo &&
|
||||
((isNodeDependence && depInfo.split(' ')?.[0] === depName) ||
|
||||
(isLinuxDependence &&
|
||||
depInfo.toLocaleLowerCase().includes('installed')) ||
|
||||
isPythonDependence) &&
|
||||
(!depVersion || depInfo.includes(depVersion))
|
||||
) {
|
||||
const endTime = dayjs();
|
||||
const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message: _message,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, _message);
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.installed },
|
||||
{ where: { id: depIds } },
|
||||
);
|
||||
return resolve(null);
|
||||
}
|
||||
}
|
||||
const dependenceProxyFileExist = await fileExist(
|
||||
config.dependenceProxyFile,
|
||||
);
|
||||
const proxyStr = dependenceProxyFileExist
|
||||
? `source ${config.dependenceProxyFile} &&`
|
||||
: '';
|
||||
const cp = spawn(
|
||||
`${proxyStr} ${depRunCommand} ${dependency.name.trim()}`,
|
||||
{
|
||||
shell: '/bin/bash',
|
||||
},
|
||||
);
|
||||
|
||||
cp.stdout.on('data', async (data) => {
|
||||
this.sockService.sendMessage({
|
||||
@@ -187,7 +317,7 @@ export default class DependenceService {
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, data.toString());
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.stderr.on('data', async (data) => {
|
||||
@@ -196,7 +326,7 @@ export default class DependenceService {
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, data.toString());
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.on('error', async (err) => {
|
||||
@@ -205,10 +335,10 @@ export default class DependenceService {
|
||||
message: JSON.stringify(err),
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, JSON.stringify(err));
|
||||
this.updateLog(depIds, JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
cp.on('exit', async (code) => {
|
||||
const endTime = dayjs();
|
||||
const isSucceed = code === 0;
|
||||
const resultText = isSucceed ? '成功' : '失败';
|
||||
@@ -221,7 +351,7 @@ export default class DependenceService {
|
||||
message,
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, message);
|
||||
this.updateLog(depIds, message);
|
||||
|
||||
let status = null;
|
||||
if (isSucceed) {
|
||||
@@ -233,7 +363,17 @@ export default class DependenceService {
|
||||
? DependenceStatus.installFailed
|
||||
: DependenceStatus.removeFailed;
|
||||
}
|
||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||
const docs = await DependenceModel.findAll({ where: { id: depIds } });
|
||||
const _docIds = docs
|
||||
.filter((x) => x.status !== DependenceStatus.cancelled)
|
||||
.map((x) => x.id!);
|
||||
|
||||
if (_docIds.length > 0) {
|
||||
await DependenceModel.update(
|
||||
{ status },
|
||||
{ where: { id: _docIds } },
|
||||
);
|
||||
}
|
||||
|
||||
// 如果删除依赖成功或者强制删除
|
||||
if ((isSucceed || force) && !isInstall) {
|
||||
|
||||
+19
-7
@@ -1,7 +1,7 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import {
|
||||
Env,
|
||||
EnvModel,
|
||||
@@ -165,7 +165,10 @@ export default class EnvService {
|
||||
|
||||
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
||||
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Env);
|
||||
if (!doc) {
|
||||
throw new Error(`Env ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async disabled(ids: string[]) {
|
||||
@@ -193,6 +196,8 @@ export default class EnvService {
|
||||
});
|
||||
const groups = groupBy(envs, 'name');
|
||||
let env_string = '';
|
||||
let js_env_string = '';
|
||||
let py_env_string = 'import os\n';
|
||||
for (const key in groups) {
|
||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||
const group = groups[key];
|
||||
@@ -202,14 +207,21 @@ export default class EnvService {
|
||||
let value = group
|
||||
.map((x) => x.value)
|
||||
.join('&')
|
||||
.replace(/(\\)[^n]/g, '\\\\')
|
||||
.replace(/(\\$)/, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/'/g, "'\\''")
|
||||
.trim();
|
||||
env_string += `export ${key}="${value}"\n`;
|
||||
env_string += `export ${key}='${value}'\n`;
|
||||
const _env_value = `'${group
|
||||
.map((x) => x.value)
|
||||
.join('&')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "\\'")}'`;
|
||||
js_env_string += `process.env.${key}=${_env_value};\n`;
|
||||
py_env_string += `os.environ['${key}']=${_env_value}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(config.envFile, env_string);
|
||||
await fs.writeFile(config.envFile, env_string);
|
||||
await fs.writeFile(config.jsEnvFile, js_env_string);
|
||||
await fs.writeFile(config.pyEnvFile, py_env_string);
|
||||
}
|
||||
}
|
||||
|
||||
+165
-43
@@ -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 got from 'got';
|
||||
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 { NotificationInfo } from '../data/notify';
|
||||
import UserService from './user';
|
||||
|
||||
@Service()
|
||||
export default class NotificationService {
|
||||
@@ -27,9 +27,12 @@ export default class NotificationService {
|
||||
['aibotk', this.aibotk],
|
||||
['iGot', this.iGot],
|
||||
['pushPlus', this.pushPlus],
|
||||
['wePlusBot', this.wePlusBot],
|
||||
['email', this.email],
|
||||
['pushMe', this.pushMe],
|
||||
['webhook', this.webhook],
|
||||
['lark', this.lark],
|
||||
['chronocat', this.chronocat],
|
||||
]);
|
||||
|
||||
private title = '';
|
||||
@@ -55,7 +58,7 @@ export default class NotificationService {
|
||||
try {
|
||||
return await notificationModeAction?.call(this);
|
||||
} catch (error: any) {
|
||||
return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -194,21 +197,35 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async bark() {
|
||||
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
||||
let {
|
||||
barkPush,
|
||||
barkIcon = '',
|
||||
barkSound = '',
|
||||
barkGroup = '',
|
||||
barkLevel = '',
|
||||
barkUrl = '',
|
||||
barkArchive = '',
|
||||
} = this.params;
|
||||
if (!barkPush.startsWith('http')) {
|
||||
barkPush = `https://api.day.app/${barkPush}`;
|
||||
}
|
||||
const url = `${barkPush}/${encodeURIComponent(
|
||||
this.title,
|
||||
)}/${encodeURIComponent(
|
||||
this.content,
|
||||
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
|
||||
|
||||
const url = `${barkPush}`;
|
||||
const body = {
|
||||
title: this.title,
|
||||
body: this.content,
|
||||
icon: barkIcon,
|
||||
sound: barkSound,
|
||||
group: barkGroup,
|
||||
isArchive: barkArchive,
|
||||
level: barkLevel,
|
||||
url: barkUrl,
|
||||
};
|
||||
try {
|
||||
const res: any = await got
|
||||
.get(url, {
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
json: body,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
.json();
|
||||
if (res.code === 200) {
|
||||
@@ -231,8 +248,8 @@ export default class NotificationService {
|
||||
telegramBotUserId,
|
||||
} = this.params;
|
||||
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
|
||||
const url = `https://${
|
||||
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
|
||||
const url = `${
|
||||
telegramBotApiHost ? telegramBotApiHost : 'https://api.telegram.org'
|
||||
}/bot${telegramBotToken}/sendMessage`;
|
||||
let agent;
|
||||
if (telegramBotProxyHost && telegramBotProxyPort) {
|
||||
@@ -303,7 +320,8 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async weWorkBot() {
|
||||
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
|
||||
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
||||
this.params;
|
||||
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||
try {
|
||||
const res: any = await got
|
||||
@@ -328,7 +346,8 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async weWorkApp() {
|
||||
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
|
||||
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
||||
this.params;
|
||||
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||
weWorkAppKey.split(',');
|
||||
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
|
||||
@@ -502,6 +521,42 @@ export default class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
private async wePlusBot() {
|
||||
const { wePlusBotToken, wePlusBotReceiver, wePlusBotVersion } = this.params;
|
||||
|
||||
let content = this.content;
|
||||
let template = 'txt';
|
||||
if (this.content.length > 800) {
|
||||
template = 'html';
|
||||
content = content.replace(/[\n\r]/g, '<br>');
|
||||
}
|
||||
|
||||
const url = `https://www.weplusbot.com/send`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
token: `${wePlusBotToken}`,
|
||||
title: `${this.title}`,
|
||||
template: `${template}`,
|
||||
content: `${content}`,
|
||||
receiver: `${wePlusBotReceiver || ''}`,
|
||||
version: `${wePlusBotVersion || 'pro'}`,
|
||||
},
|
||||
})
|
||||
.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 lark() {
|
||||
let { larkKey } = this.params;
|
||||
|
||||
@@ -520,7 +575,7 @@ export default class NotificationService {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
.json();
|
||||
if (res.StatusCode === 0) {
|
||||
if (res.StatusCode === 0 || res.code === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
@@ -556,6 +611,85 @@ export default class NotificationService {
|
||||
} else {
|
||||
throw new Error(JSON.stringify(info));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async pushMe() {
|
||||
const { pushMeKey, pushMeUrl } = this.params;
|
||||
try {
|
||||
const res: any = await got.post(pushMeUrl || 'https://push.i-i.me/', {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
push_key: pushMeKey,
|
||||
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, chronocatToken } = 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 ${chronocatToken}`,
|
||||
};
|
||||
|
||||
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.statusCode === 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(res.body);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
@@ -570,15 +704,14 @@ export default class NotificationService {
|
||||
webhookContentType,
|
||||
} = this.params;
|
||||
|
||||
const { formatBody, formatUrl } = this.formatNotifyContent(
|
||||
webhookUrl,
|
||||
webhookBody,
|
||||
);
|
||||
if (!formatUrl && !formatBody) {
|
||||
return false;
|
||||
if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) {
|
||||
throw new Error('Url 或者 Body 中必须包含 $title');
|
||||
}
|
||||
|
||||
const headers = parseHeaders(webhookHeaders);
|
||||
const body = parseBody(formatBody, webhookContentType);
|
||||
const body = parseBody(webhookBody, webhookContentType, (v) =>
|
||||
v?.replaceAll('$title', this.title)?.replaceAll('$content', this.content),
|
||||
);
|
||||
const bodyParam = this.formatBody(webhookContentType, body);
|
||||
const options = {
|
||||
method: webhookMethod,
|
||||
@@ -588,6 +721,9 @@ export default class NotificationService {
|
||||
...bodyParam,
|
||||
};
|
||||
try {
|
||||
const formatUrl = webhookUrl
|
||||
?.replaceAll('$title', encodeURIComponent(this.title))
|
||||
?.replaceAll('$content', encodeURIComponent(this.content));
|
||||
const res = await got(formatUrl, options);
|
||||
if (String(res.statusCode).startsWith('20')) {
|
||||
return true;
|
||||
@@ -607,23 +743,9 @@ export default class NotificationService {
|
||||
case 'multipart/form-data':
|
||||
return { form: body };
|
||||
case 'application/x-www-form-urlencoded':
|
||||
case 'text/plain':
|
||||
return { body };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private formatNotifyContent(url: string, body: string) {
|
||||
if (!url.includes('$title') && !body.includes('$title')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
formatUrl: url
|
||||
?.replaceAll('$title', encodeURIComponent(this.title))
|
||||
?.replaceAll('$content', encodeURIComponent(this.content)),
|
||||
formatBody: body
|
||||
?.replaceAll('$title', this.title)
|
||||
?.replaceAll('$content', this.content),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class OpenService {
|
||||
|
||||
public async insert(payload: App): Promise<App> {
|
||||
const doc = await AppModel.create(payload, { returning: true });
|
||||
return doc.get({ plain: true }) as App;
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async update(payload: App): Promise<App> {
|
||||
@@ -45,7 +45,10 @@ export default class OpenService {
|
||||
|
||||
public async getDb(query: any): Promise<App> {
|
||||
const doc: any = await AppModel.findOne({ where: query });
|
||||
return doc && (doc.get({ plain: true }) as App);
|
||||
if (!doc) {
|
||||
throw new Error(`App ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
|
||||
+40
-29
@@ -12,8 +12,8 @@ import dayjs from 'dayjs';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import { spawn } from 'cross-spawn';
|
||||
|
||||
interface ScheduleTaskType {
|
||||
id: number;
|
||||
export interface ScheduleTaskType {
|
||||
id?: number;
|
||||
command: string;
|
||||
name?: string;
|
||||
schedule?: string;
|
||||
@@ -47,10 +47,17 @@ export default class ScheduleService {
|
||||
async runTask(
|
||||
command: string,
|
||||
callbacks: TaskCallbacks = {},
|
||||
params: {
|
||||
schedule?: string;
|
||||
name?: string;
|
||||
command?: string;
|
||||
},
|
||||
completionTime: 'start' | 'end' = 'end',
|
||||
) {
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
return taskLimit.runWithCronLimit(() => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
|
||||
|
||||
try {
|
||||
const startTime = dayjs();
|
||||
await callbacks.onBefore?.(startTime);
|
||||
@@ -66,9 +73,8 @@ export default class ScheduleService {
|
||||
|
||||
cp.stderr.on('data', async (data) => {
|
||||
this.logger.info(
|
||||
'[执行任务失败] %s,时间:%s, 错误信息:%j',
|
||||
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
data.toString(),
|
||||
);
|
||||
await callbacks.onError?.(data.toString());
|
||||
@@ -76,40 +82,32 @@ export default class ScheduleService {
|
||||
|
||||
cp.on('error', async (err) => {
|
||||
this.logger.error(
|
||||
'[创建任务失败] %s,时间:%s, 错误信息:%j',
|
||||
'[panel][创建任务失败] 命令: %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) => {
|
||||
cp.on('exit', async (code) => {
|
||||
const endTime = dayjs();
|
||||
await callbacks.onEnd?.(
|
||||
cp,
|
||||
endTime,
|
||||
endTime.diff(startTime, 'seconds'),
|
||||
);
|
||||
resolve(null);
|
||||
resolve({ ...params, pid: cp.pid, code });
|
||||
});
|
||||
} catch (error) {
|
||||
await this.logger.error(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
this.logger.error(
|
||||
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
error,
|
||||
);
|
||||
await callbacks.onError?.(JSON.stringify(error));
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async createCronTask(
|
||||
@@ -119,7 +117,7 @@ export default class ScheduleService {
|
||||
) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info(
|
||||
'[创建cron任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
||||
'[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
|
||||
_id,
|
||||
schedule,
|
||||
name,
|
||||
@@ -129,18 +127,26 @@ export default class ScheduleService {
|
||||
this.scheduleStacks.set(
|
||||
_id,
|
||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||
this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
schedule,
|
||||
command,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if (runImmediately) {
|
||||
this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
schedule,
|
||||
command,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||
this.logger.info('[panel][取消定时任务], 任务名: %s', name);
|
||||
if (this.scheduleStacks.has(_id)) {
|
||||
this.scheduleStacks.get(_id)?.cancel();
|
||||
this.scheduleStacks.delete(_id);
|
||||
@@ -155,7 +161,7 @@ export default class ScheduleService {
|
||||
) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info(
|
||||
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
|
||||
'[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s',
|
||||
_id,
|
||||
name,
|
||||
command,
|
||||
@@ -163,13 +169,15 @@ export default class ScheduleService {
|
||||
const task = new Task(
|
||||
name,
|
||||
() => {
|
||||
this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
command,
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
this.logger.error(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
'[执行任务失败] 命令: %s, 错误信息: %j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
err,
|
||||
);
|
||||
},
|
||||
@@ -184,13 +192,16 @@ export default class ScheduleService {
|
||||
this.intervalSchedule.addIntervalJob(job);
|
||||
|
||||
if (runImmediately) {
|
||||
this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
command,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
|
||||
this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name);
|
||||
this.intervalSchedule.removeById(_id);
|
||||
}
|
||||
|
||||
|
||||
+16
-10
@@ -1,13 +1,12 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import path, { join } from 'path';
|
||||
import SockService from './sock';
|
||||
import CronService from './cron';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import config from '../config';
|
||||
import { TASK_COMMAND } from '../config/const';
|
||||
import { getPid, killTask } from '../config/util';
|
||||
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
|
||||
|
||||
@Service()
|
||||
export default class ScriptService {
|
||||
@@ -21,9 +20,7 @@ export default class ScriptService {
|
||||
private taskCallbacks(filePath: string): TaskCallbacks {
|
||||
return {
|
||||
onEnd: async (cp, endTime, diff) => {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch (error) {}
|
||||
await rmPath(filePath);
|
||||
},
|
||||
onError: async (message: string) => {
|
||||
this.sockService.sendMessage({
|
||||
@@ -42,10 +39,11 @@ export default class ScriptService {
|
||||
|
||||
public async runScript(filePath: string) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
const command = `${TASK_COMMAND} -l ${relativePath} now`;
|
||||
const command = `${TASK_COMMAND} ${relativePath} now`;
|
||||
const pid = await this.scheduleService.runTask(
|
||||
command,
|
||||
`real_time=true ${command}`,
|
||||
this.taskCallbacks(filePath),
|
||||
{ command },
|
||||
'start',
|
||||
);
|
||||
|
||||
@@ -53,10 +51,9 @@ export default class ScriptService {
|
||||
}
|
||||
|
||||
public async stopScript(filePath: string, pid: number) {
|
||||
let str = '';
|
||||
if (!pid) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
|
||||
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
|
||||
}
|
||||
try {
|
||||
await killTask(pid);
|
||||
@@ -64,4 +61,13 @@ export default class ScriptService {
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async getFile(filePath: string, fileName: string) {
|
||||
let _filePath = join(config.scriptPath, filePath, fileName);
|
||||
if (filePath.startsWith(config.dataPath)) {
|
||||
_filePath = join(filePath, fileName);
|
||||
}
|
||||
const content = await getFileContentByName(_filePath);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SockMessage } from '../data/sock';
|
||||
export default class SockService {
|
||||
private clients: Connection[] = [];
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||
|
||||
public getClients() {
|
||||
return this.clients;
|
||||
|
||||
+42
-33
@@ -1,11 +1,12 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import fs, { existsSync } from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { Subscription } from '../data/subscription';
|
||||
import { formatUrl } from '../config/subscription';
|
||||
import config from '../config';
|
||||
import { fileExist, rmPath } from '../config/util';
|
||||
|
||||
@Service()
|
||||
export default class SshKeyService {
|
||||
@@ -18,15 +19,16 @@ export default class SshKeyService {
|
||||
this.initSshConfigFile();
|
||||
}
|
||||
|
||||
private initSshConfigFile() {
|
||||
private async initSshConfigFile() {
|
||||
let config = '';
|
||||
if (existsSync(this.sshConfigFilePath)) {
|
||||
config = fs.readFileSync(this.sshConfigFilePath, { encoding: 'utf-8' });
|
||||
const _exist = await fileExist(this.sshConfigFilePath);
|
||||
if (_exist) {
|
||||
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
|
||||
} else {
|
||||
fs.writeFileSync(this.sshConfigFilePath, '');
|
||||
await fs.writeFile(this.sshConfigFilePath, '');
|
||||
}
|
||||
if (!config.includes(this.sshConfigHeader)) {
|
||||
fs.writeFileSync(
|
||||
await fs.writeFile(
|
||||
this.sshConfigFilePath,
|
||||
`${this.sshConfigHeader}\n\n${config}`,
|
||||
{ encoding: 'utf-8' },
|
||||
@@ -34,9 +36,12 @@ export default class SshKeyService {
|
||||
}
|
||||
}
|
||||
|
||||
private generatePrivateKeyFile(alias: string, key: string): void {
|
||||
private async generatePrivateKeyFile(
|
||||
alias: string,
|
||||
key: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
|
||||
await fs.writeFile(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
|
||||
encoding: 'utf8',
|
||||
mode: '400',
|
||||
});
|
||||
@@ -45,20 +50,22 @@ export default class SshKeyService {
|
||||
}
|
||||
}
|
||||
|
||||
private removePrivateKeyFile(alias: string): void {
|
||||
private async removePrivateKeyFile(alias: string): Promise<void> {
|
||||
try {
|
||||
const filePath = path.join(this.sshPath, alias);
|
||||
if (existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
await rmPath(filePath);
|
||||
} catch (error) {
|
||||
this.logger.error('删除私钥文件失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
|
||||
private async generateSingleSshConfig(
|
||||
alias: string,
|
||||
host: string,
|
||||
proxy?: string,
|
||||
) {
|
||||
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 2>/dev/null\n`
|
||||
@@ -67,49 +74,51 @@ export default class SshKeyService {
|
||||
this.sshPath,
|
||||
alias,
|
||||
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
||||
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
await fs.writeFile(
|
||||
`${path.join(this.sshPath, `${alias}.config`)}`,
|
||||
config,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private removeSshConfig(alias: string) {
|
||||
private async removeSshConfig(alias: string) {
|
||||
try {
|
||||
const filePath = path.join(this.sshPath, `${alias}.config`);
|
||||
if (existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
await rmPath(filePath);
|
||||
} catch (error) {
|
||||
this.logger.error(`删除ssh配置文件${alias}失败`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public addSSHKey(
|
||||
public async addSSHKey(
|
||||
key: string,
|
||||
alias: string,
|
||||
host: string,
|
||||
proxy?: string,
|
||||
): void {
|
||||
this.generatePrivateKeyFile(alias, key);
|
||||
this.generateSingleSshConfig(alias, host, proxy);
|
||||
): Promise<void> {
|
||||
await this.generatePrivateKeyFile(alias, key);
|
||||
await this.generateSingleSshConfig(alias, host, proxy);
|
||||
}
|
||||
|
||||
public removeSSHKey(alias: string, host: string, proxy?: string): void {
|
||||
this.removePrivateKeyFile(alias);
|
||||
this.removeSshConfig(alias);
|
||||
public async removeSSHKey(alias: string, host: string, proxy?: string): Promise<void> {
|
||||
await this.removePrivateKeyFile(alias);
|
||||
await this.removeSshConfig(alias);
|
||||
}
|
||||
|
||||
public setSshConfig(docs: Subscription[]) {
|
||||
public async 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(
|
||||
await this.removePrivateKeyFile(alias);
|
||||
await this.removeSshConfig(alias);
|
||||
await this.generatePrivateKeyFile(
|
||||
alias,
|
||||
(doc.pull_option as any).private_key,
|
||||
);
|
||||
this.generateSingleSshConfig(alias, host, proxy);
|
||||
await this.generateSingleSshConfig(alias, host, proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
SubscriptionStatus,
|
||||
} from '../data/subscription';
|
||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import {
|
||||
getFileContentByName,
|
||||
concurrentRun,
|
||||
@@ -16,10 +15,11 @@ import {
|
||||
killTask,
|
||||
handleLogPath,
|
||||
promiseExec,
|
||||
rmPath,
|
||||
} from '../config/util';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import { FindOptions, Op } from 'sequelize';
|
||||
import path from 'path';
|
||||
import path, { join } from 'path';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
||||
import SockService from './sock';
|
||||
@@ -27,6 +27,8 @@ import SshKeyService from './sshKey';
|
||||
import dayjs from 'dayjs';
|
||||
import { LOG_END_SYMBOL } from '../config/const';
|
||||
import { formatCommand, formatUrl } from '../config/subscription';
|
||||
import { CrontabModel } from '../data/cron';
|
||||
import CrontabService from './cron';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionService {
|
||||
@@ -35,6 +37,7 @@ export default class SubscriptionService {
|
||||
private scheduleService: ScheduleService,
|
||||
private sockService: SockService,
|
||||
private sshKeyService: SshKeyService,
|
||||
private crontabService: CrontabService,
|
||||
) {}
|
||||
|
||||
public async list(searchText?: string): Promise<Subscription[]> {
|
||||
@@ -88,9 +91,9 @@ export default class SubscriptionService {
|
||||
this.taskCallbacks(doc),
|
||||
runImmediately,
|
||||
));
|
||||
} else {
|
||||
} else if (doc.interval_schedule) {
|
||||
this.scheduleService.cancelIntervalTask(doc as any);
|
||||
const { type, value } = doc.interval_schedule as any;
|
||||
const { type, value } = doc.interval_schedule;
|
||||
needCreate &&
|
||||
(await this.scheduleService.createIntervalTask(
|
||||
doc as any,
|
||||
@@ -103,7 +106,7 @@ export default class SubscriptionService {
|
||||
|
||||
public async setSshConfig() {
|
||||
const docs = await SubscriptionModel.findAll();
|
||||
this.sshKeyService.setSshConfig(docs);
|
||||
await this.sshKeyService.setSshConfig(docs);
|
||||
}
|
||||
|
||||
private taskCallbacks(doc: Subscription): TaskCallbacks {
|
||||
@@ -127,7 +130,7 @@ export default class SubscriptionService {
|
||||
let beforeStr = '';
|
||||
try {
|
||||
if (doc.sub_before) {
|
||||
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
|
||||
await fs.appendFile(absolutePath, `\n## 执行before命令...\n\n`);
|
||||
beforeStr = await promiseExec(doc.sub_before);
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -135,7 +138,7 @@ export default class SubscriptionService {
|
||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
||||
}
|
||||
if (beforeStr) {
|
||||
fs.appendFileSync(absolutePath, `${beforeStr}\n`);
|
||||
await fs.appendFile(absolutePath, `${beforeStr}\n`);
|
||||
}
|
||||
},
|
||||
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
|
||||
@@ -154,7 +157,7 @@ export default class SubscriptionService {
|
||||
let afterStr = '';
|
||||
try {
|
||||
if (sub.sub_after) {
|
||||
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
|
||||
await fs.appendFile(absolutePath, `\n\n## 执行after命令...\n\n`);
|
||||
afterStr = await promiseExec(sub.sub_after);
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -162,10 +165,10 @@ export default class SubscriptionService {
|
||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
||||
}
|
||||
if (afterStr) {
|
||||
fs.appendFileSync(absolutePath, `${afterStr}\n`);
|
||||
await fs.appendFile(absolutePath, `${afterStr}\n`);
|
||||
}
|
||||
|
||||
fs.appendFileSync(
|
||||
await fs.appendFile(
|
||||
absolutePath,
|
||||
`\n## 执行结束... ${endTime.format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
@@ -186,12 +189,12 @@ export default class SubscriptionService {
|
||||
onError: async (message: string) => {
|
||||
const sub = await this.getDb({ id: doc.id });
|
||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
await fs.appendFile(absolutePath, `\n${message}`);
|
||||
},
|
||||
onLog: async (message: string) => {
|
||||
const sub = await this.getDb({ id: doc.id });
|
||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||
fs.appendFileSync(absolutePath, `\n${message}`);
|
||||
await fs.appendFile(absolutePath, `\n${message}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -253,20 +256,34 @@ export default class SubscriptionService {
|
||||
);
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
public async remove(ids: number[], query: { force?: boolean }) {
|
||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
await this.handleTask(doc, false);
|
||||
}
|
||||
await SubscriptionModel.destroy({ where: { id: ids } });
|
||||
await this.setSshConfig();
|
||||
|
||||
if (query?.force === true) {
|
||||
const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
|
||||
if (crons?.length) {
|
||||
await this.crontabService.remove(crons.map((x) => x.id!));
|
||||
}
|
||||
for (const doc of docs) {
|
||||
const filePath = join(config.scriptPath, doc.alias);
|
||||
await rmPath(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getDb(
|
||||
query: FindOptions<Subscription>['where'],
|
||||
): Promise<Subscription> {
|
||||
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Subscription);
|
||||
const doc = await SubscriptionModel.findOne({ where: { ...query } });
|
||||
if (!doc) {
|
||||
throw new Error(`Subscription ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async run(ids: number[]) {
|
||||
@@ -286,17 +303,9 @@ export default class SubscriptionService {
|
||||
try {
|
||||
await killTask(doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.silly(error);
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
const absolutePath = await handleLogPath(doc.log_path as string);
|
||||
|
||||
fs.appendFileSync(
|
||||
`${absolutePath}`,
|
||||
`\n## 执行结束... ${dayjs().format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)}${LOG_END_SYMBOL}`,
|
||||
);
|
||||
}
|
||||
|
||||
await SubscriptionModel.update(
|
||||
@@ -313,7 +322,11 @@ export default class SubscriptionService {
|
||||
|
||||
const command = formatCommand(subscription);
|
||||
|
||||
this.scheduleService.runTask(command, this.taskCallbacks(subscription));
|
||||
this.scheduleService.runTask(command, this.taskCallbacks(subscription), {
|
||||
name: subscription.name,
|
||||
schedule: subscription.schedule,
|
||||
command,
|
||||
});
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
@@ -341,7 +354,7 @@ export default class SubscriptionService {
|
||||
}
|
||||
|
||||
const absolutePath = await handleLogPath(doc.log_path as string);
|
||||
return getFileContentByName(absolutePath);
|
||||
return await getFileContentByName(absolutePath);
|
||||
}
|
||||
|
||||
public async logs(id: number) {
|
||||
@@ -353,15 +366,18 @@ export default class SubscriptionService {
|
||||
if (doc.log_path) {
|
||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||
const dir = path.resolve(config.logPath, relativeDir);
|
||||
if (existsSync(dir)) {
|
||||
let files = await promises.readdir(dir);
|
||||
return files
|
||||
.map((x) => ({
|
||||
filename: x,
|
||||
directory: relativeDir.replace(config.logPath, ''),
|
||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
||||
}))
|
||||
.sort((a, b) => b.time - a.time);
|
||||
const _exist = await fileExist(dir);
|
||||
if (_exist) {
|
||||
let files = await fs.readdir(dir);
|
||||
return (
|
||||
await Promise.all(
|
||||
files.map(async (x) => ({
|
||||
filename: x,
|
||||
directory: relativeDir.replace(config.logPath, ''),
|
||||
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(),
|
||||
})),
|
||||
)
|
||||
).sort((a, b) => b.time - a.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+252
-40
@@ -1,22 +1,38 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { spawn } from 'cross-spawn';
|
||||
import { Response } from 'express';
|
||||
import fs from 'fs';
|
||||
import got from 'got';
|
||||
import sum from 'lodash/sum';
|
||||
import path from 'path';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import { AuthDataType, AuthInfo, AuthModel, AuthModelInfo } from '../data/auth';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import NotificationService from './notify';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import { spawn } from 'cross-spawn';
|
||||
import SockService from './sock';
|
||||
import got from 'got';
|
||||
import { TASK_COMMAND } from '../config/const';
|
||||
import {
|
||||
getPid,
|
||||
killTask,
|
||||
parseContentVersion,
|
||||
parseVersion,
|
||||
promiseExec,
|
||||
readDirs,
|
||||
} from '../config/util';
|
||||
import { TASK_COMMAND } from '../config/const';
|
||||
import taskLimit from '../shared/pLimit'
|
||||
import {
|
||||
DependenceModel,
|
||||
DependenceStatus,
|
||||
DependenceTypes,
|
||||
} from '../data/dependence';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import {
|
||||
AuthDataType,
|
||||
SystemInfo,
|
||||
SystemInstance,
|
||||
SystemModel,
|
||||
SystemModelInfo,
|
||||
} from '../data/system';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import NotificationService from './notify';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import SockService from './sock';
|
||||
|
||||
@Service()
|
||||
export default class SystemService {
|
||||
@@ -31,18 +47,21 @@ export default class SystemService {
|
||||
|
||||
public async getSystemConfig() {
|
||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||
return doc || {};
|
||||
return doc;
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
await AuthModel.upsert({ ...payload });
|
||||
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
|
||||
await SystemModel.upsert({ ...payload });
|
||||
const doc = await this.getDb({ type: payload.type });
|
||||
return doc;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<any> {
|
||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as any);
|
||||
public async getDb(query: any): Promise<SystemInfo> {
|
||||
const doc = await SystemModel.findOne({ where: { ...query } });
|
||||
if (!doc) {
|
||||
throw new Error(`System ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||
@@ -63,32 +82,174 @@ export default class SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateSystemConfig(info: AuthModelInfo) {
|
||||
public async updateLogRemoveFrequency(info: SystemModelInfo) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
const result = await this.updateAuthDb({
|
||||
...oDoc,
|
||||
type: AuthDataType.systemConfig,
|
||||
info,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
if (info.logRemoveFrequency) {
|
||||
const cron = {
|
||||
id: result.id,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${info.logRemoveFrequency}`,
|
||||
};
|
||||
const cron = {
|
||||
id: result.id || NaN,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${info.logRemoveFrequency}`,
|
||||
};
|
||||
if (oDoc.info?.logRemoveFrequency) {
|
||||
await this.scheduleService.cancelIntervalTask(cron);
|
||||
if (info.logRemoveFrequency > 0) {
|
||||
this.scheduleService.createIntervalTask(cron, {
|
||||
days: info.logRemoveFrequency,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {
|
||||
this.scheduleService.createIntervalTask(cron, {
|
||||
days: info.logRemoveFrequency,
|
||||
});
|
||||
}
|
||||
return { code: 200, data: info };
|
||||
}
|
||||
|
||||
public async updateCronConcurrency(info: SystemModelInfo) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
if (info.cronConcurrency) {
|
||||
await taskLimit.setCustomLimit(info.cronConcurrency);
|
||||
}
|
||||
return { code: 200, data: info };
|
||||
}
|
||||
|
||||
public async updateDependenceProxy(info: SystemModelInfo) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
if (info.dependenceProxy) {
|
||||
await fs.promises.writeFile(
|
||||
config.dependenceProxyFile,
|
||||
`export http_proxy="${info.dependenceProxy}"\nexport https_proxy="${info.dependenceProxy}"`,
|
||||
);
|
||||
} else {
|
||||
await fs.promises.rm(config.dependenceProxyFile);
|
||||
}
|
||||
return { code: 200, data: info };
|
||||
}
|
||||
|
||||
public async updateNodeMirror(info: SystemModelInfo, res?: Response) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
let cmd = 'pnpm config delete registry';
|
||||
if (info.nodeMirror) {
|
||||
cmd = `pnpm config set registry ${info.nodeMirror}`;
|
||||
}
|
||||
let command = `cd && ${cmd}`;
|
||||
const docs = await DependenceModel.findAll({
|
||||
where: {
|
||||
type: DependenceTypes.nodejs,
|
||||
status: DependenceStatus.installed,
|
||||
},
|
||||
});
|
||||
if (docs.length > 0) {
|
||||
command += ` && pnpm i -g`;
|
||||
}
|
||||
this.scheduleService.runTask(
|
||||
command,
|
||||
{
|
||||
onStart: async (cp) => {
|
||||
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
|
||||
res?.end();
|
||||
},
|
||||
onEnd: async () => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateNodeMirror',
|
||||
message: 'update node mirror end',
|
||||
});
|
||||
},
|
||||
onError: async (message: string) => {
|
||||
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
|
||||
},
|
||||
onLog: async (message: string) => {
|
||||
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
|
||||
},
|
||||
},
|
||||
{
|
||||
command,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async updatePythonMirror(info: SystemModelInfo) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
let cmd = 'pip config unset global.index-url';
|
||||
if (info.pythonMirror) {
|
||||
cmd = `pip3 config set global.index-url ${info.pythonMirror}`;
|
||||
}
|
||||
await promiseExec(cmd);
|
||||
return { code: 200, data: info };
|
||||
}
|
||||
|
||||
public async updateLinuxMirror(
|
||||
info: SystemModelInfo,
|
||||
res?: Response,
|
||||
onEnd?: () => void,
|
||||
) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
|
||||
let targetDomain = 'https://dl-cdn.alpinelinux.org';
|
||||
const content = await fs.promises.readFile('/etc/apk/repositories', {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
|
||||
if (domainMatch) {
|
||||
defaultDomain = domainMatch[1];
|
||||
}
|
||||
if (info.linuxMirror) {
|
||||
targetDomain = info.linuxMirror;
|
||||
}
|
||||
const command = `sed -i 's/${defaultDomain.replace(
|
||||
/\//g,
|
||||
'\\/',
|
||||
)}/${targetDomain.replace(
|
||||
/\//g,
|
||||
'\\/',
|
||||
)}/g' /etc/apk/repositories && apk update -f`;
|
||||
|
||||
this.scheduleService.runTask(
|
||||
command,
|
||||
{
|
||||
onStart: async (cp) => {
|
||||
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
|
||||
res?.end();
|
||||
},
|
||||
onEnd: async () => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateLinuxMirror',
|
||||
message: 'update linux mirror end',
|
||||
});
|
||||
onEnd?.();
|
||||
},
|
||||
onError: async (message: string) => {
|
||||
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
|
||||
},
|
||||
onLog: async (message: string) => {
|
||||
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
|
||||
},
|
||||
},
|
||||
{
|
||||
command,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async checkUpdate() {
|
||||
try {
|
||||
const currentVersionContent = await parseVersion(config.versionFile);
|
||||
@@ -148,7 +309,7 @@ export default class SystemService {
|
||||
}
|
||||
|
||||
public async updateSystem() {
|
||||
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
||||
const cp = spawn('real_time=true ql update false', { shell: '/bin/bash' });
|
||||
|
||||
cp.stdout.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
@@ -174,6 +335,13 @@ export default class SystemService {
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async reloadSystem(target?: 'system' | 'data') {
|
||||
const cmd = `real_time=true ql reload ${target || ''}`;
|
||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||
cp.unref();
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async notify({ title, content }: { title: string; content: string }) {
|
||||
const isSuccess = await this.notificationService.notify(title, content);
|
||||
if (isSuccess) {
|
||||
@@ -183,17 +351,13 @@ export default class SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
public async run(
|
||||
{ command, logPath }: { command: string; logPath: string },
|
||||
callback: TaskCallbacks,
|
||||
) {
|
||||
public async run({ command }: { command: string }, callback: TaskCallbacks) {
|
||||
if (!command.startsWith(TASK_COMMAND)) {
|
||||
command = `${TASK_COMMAND} ${command}`;
|
||||
}
|
||||
this.scheduleService.runTask(
|
||||
`real_log_path=${logPath} real_time=true ${command}`,
|
||||
callback,
|
||||
);
|
||||
this.scheduleService.runTask(`real_time=true ${command}`, callback, {
|
||||
command,
|
||||
});
|
||||
}
|
||||
|
||||
public async stop({ command, pid }: { command: string; pid: number }) {
|
||||
@@ -217,4 +381,52 @@ export default class SystemService {
|
||||
return { code: 400, message: '任务未找到' };
|
||||
}
|
||||
}
|
||||
|
||||
public async exportData(res: Response) {
|
||||
try {
|
||||
await promiseExec(
|
||||
`cd ${config.rootPath} && tar -zcvf ${config.dataTgzFile} 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')}`);
|
||||
const res = await promiseExec(
|
||||
`cd ${config.tmpPath} && tar -zxvf data.tgz`,
|
||||
);
|
||||
return { code: 200, data: res };
|
||||
} catch (error: any) {
|
||||
return { code: 400, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
public async getSystemLog(res: Response) {
|
||||
const result = await 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);
|
||||
}
|
||||
}
|
||||
|
||||
+71
-45
@@ -1,22 +1,33 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import {
|
||||
createFile,
|
||||
createRandomString,
|
||||
fileExist,
|
||||
getNetIp,
|
||||
getPlatform,
|
||||
safeJSONParse,
|
||||
} from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { authenticator } from '@otplib/preset-default';
|
||||
import { AuthDataType, AuthInfo, AuthModel, AuthModelInfo, LoginStatus } from '../data/auth';
|
||||
import {
|
||||
AuthDataType,
|
||||
SystemInfo,
|
||||
SystemModel,
|
||||
SystemModelInfo,
|
||||
LoginStatus,
|
||||
} from '../data/system';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import NotificationService from './notify';
|
||||
import { Request } from 'express';
|
||||
import ScheduleService from './schedule';
|
||||
import SockService from './sock';
|
||||
import dayjs from 'dayjs';
|
||||
import IP2Region from 'ip2region';
|
||||
import requestIp from 'request-ip';
|
||||
import uniq from 'lodash/uniq';
|
||||
|
||||
@Service()
|
||||
export default class UserService {
|
||||
@@ -27,7 +38,7 @@ export default class UserService {
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private scheduleService: ScheduleService,
|
||||
private sockService: SockService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
public async login(
|
||||
payloads: {
|
||||
@@ -37,12 +48,13 @@ export default class UserService {
|
||||
req: Request,
|
||||
needTwoFactor = true,
|
||||
): Promise<any> {
|
||||
if (!fs.existsSync(config.authConfigFile)) {
|
||||
const _exist = await fileExist(config.authConfigFile);
|
||||
if (!_exist) {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
|
||||
let { username, password } = payloads;
|
||||
const content = this.getAuthInfo();
|
||||
const content = await this.getAuthInfo();
|
||||
const timestamp = Date.now();
|
||||
if (content) {
|
||||
let {
|
||||
@@ -95,7 +107,16 @@ export default class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
const { ip, address } = await getNetIp(req);
|
||||
const ip = requestIp.getClientIp(req) || '';
|
||||
const query = new IP2Region();
|
||||
const ipAddress = query.search(ip);
|
||||
let address = '';
|
||||
if (ipAddress) {
|
||||
const { country, province, city, isp } = ipAddress;
|
||||
address = uniq([country, province, city, isp])
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
if (username === cUsername && password === cPassword) {
|
||||
const data = createRandomString(50, 100);
|
||||
const expiration = twoFactorActivated ? 60 : 20;
|
||||
@@ -117,12 +138,12 @@ export default class UserService {
|
||||
platform: req.platform,
|
||||
isTwoFactorChecking: false,
|
||||
});
|
||||
await this.notificationService.notify(
|
||||
this.notificationService.notify(
|
||||
'登录通知',
|
||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${req.platform
|
||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||
req.platform
|
||||
}端 登录成功,ip地址 ${ip}`,
|
||||
);
|
||||
await this.getLoginLog();
|
||||
await this.insertDb({
|
||||
type: AuthDataType.loginLog,
|
||||
info: {
|
||||
@@ -145,12 +166,12 @@ export default class UserService {
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
});
|
||||
await this.notificationService.notify(
|
||||
this.notificationService.notify(
|
||||
'登录通知',
|
||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${req.platform
|
||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||
req.platform
|
||||
}端 登录失败,ip地址 ${ip}`,
|
||||
);
|
||||
await this.getLoginLog();
|
||||
await this.insertDb({
|
||||
type: AuthDataType.loginLog,
|
||||
info: {
|
||||
@@ -178,21 +199,23 @@ export default class UserService {
|
||||
}
|
||||
|
||||
public async logout(platform: string): Promise<any> {
|
||||
const authInfo = this.getAuthInfo();
|
||||
const authInfo = await this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, {
|
||||
token: '',
|
||||
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||
});
|
||||
}
|
||||
|
||||
public async getLoginLog(): Promise<Array<AuthModelInfo | undefined>> {
|
||||
const docs = await AuthModel.findAll({
|
||||
public async getLoginLog(): Promise<Array<SystemModelInfo | undefined>> {
|
||||
const docs = await SystemModel.findAll({
|
||||
where: { type: AuthDataType.loginLog },
|
||||
});
|
||||
if (docs && docs.length > 0) {
|
||||
const result = docs.sort((a, b) => b.info!.timestamp! - a.info!.timestamp!);
|
||||
const result = docs.sort(
|
||||
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
|
||||
);
|
||||
if (result.length > 100) {
|
||||
await AuthModel.destroy({
|
||||
await SystemModel.destroy({
|
||||
where: { id: result[result.length - 1].id },
|
||||
});
|
||||
}
|
||||
@@ -201,13 +224,13 @@ export default class UserService {
|
||||
return [];
|
||||
}
|
||||
|
||||
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
|
||||
const doc = await AuthModel.create({ ...payload }, { returning: true });
|
||||
private async insertDb(payload: SystemInfo): Promise<SystemInfo> {
|
||||
const doc = await SystemModel.create({ ...payload }, { returning: true });
|
||||
return doc;
|
||||
}
|
||||
|
||||
private initAuthInfo() {
|
||||
fs.writeFileSync(
|
||||
private async initAuthInfo() {
|
||||
await fs.writeFile(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
@@ -230,13 +253,13 @@ export default class UserService {
|
||||
if (password === 'admin') {
|
||||
return { code: 400, message: '密码不能设置为admin' };
|
||||
}
|
||||
const authInfo = this.getAuthInfo();
|
||||
const authInfo = await this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, { username, password });
|
||||
return { code: 200, message: '更新成功' };
|
||||
}
|
||||
|
||||
public async updateAvatar(avatar: string) {
|
||||
const authInfo = this.getAuthInfo();
|
||||
const authInfo = await this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, { avatar });
|
||||
return { code: 200, data: avatar, message: '更新成功' };
|
||||
}
|
||||
@@ -244,7 +267,7 @@ export default class UserService {
|
||||
public async getUserInfo(): Promise<any> {
|
||||
const authFileExist = await fileExist(config.authConfigFile);
|
||||
if (!authFileExist) {
|
||||
fs.writeFileSync(
|
||||
await createFile(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
@@ -252,19 +275,19 @@ export default class UserService {
|
||||
}),
|
||||
);
|
||||
}
|
||||
return this.getAuthInfo();
|
||||
return await this.getAuthInfo();
|
||||
}
|
||||
|
||||
public initTwoFactor() {
|
||||
public async initTwoFactor() {
|
||||
const secret = authenticator.generateSecret();
|
||||
const authInfo = this.getAuthInfo();
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
||||
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||
return { secret, url: otpauth };
|
||||
}
|
||||
|
||||
public activeTwoFactor(code: string) {
|
||||
const authInfo = this.getAuthInfo();
|
||||
public async activeTwoFactor(code: string) {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const isValid = authenticator.verify({
|
||||
token: code,
|
||||
secret: authInfo.twoFactorSecret,
|
||||
@@ -283,7 +306,7 @@ export default class UserService {
|
||||
}: { username: string; password: string; code: string },
|
||||
req: any,
|
||||
) {
|
||||
const authInfo = this.getAuthInfo();
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
|
||||
if (!isTwoFactorChecking) {
|
||||
return { code: 450, message: '未知错误' };
|
||||
@@ -305,8 +328,8 @@ export default class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
public deactiveTwoFactor() {
|
||||
const authInfo = this.getAuthInfo();
|
||||
public async deactiveTwoFactor() {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, {
|
||||
twoFactorActivated: false,
|
||||
twoFactorActived: false,
|
||||
@@ -315,13 +338,13 @@ export default class UserService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private getAuthInfo() {
|
||||
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
return JSON.parse(content || '{}');
|
||||
private async getAuthInfo() {
|
||||
const content = await fs.readFile(config.authConfigFile, 'utf8');
|
||||
return safeJSONParse(content);
|
||||
}
|
||||
|
||||
private updateAuthInfo(authInfo: any, info: any) {
|
||||
fs.writeFileSync(
|
||||
private async updateAuthInfo(authInfo: any, info: any) {
|
||||
await fs.writeFile(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({ ...authInfo, ...info }),
|
||||
);
|
||||
@@ -329,26 +352,29 @@ export default class UserService {
|
||||
|
||||
public async getNotificationMode(): Promise<NotificationInfo> {
|
||||
const doc = await this.getDb({ type: AuthDataType.notification });
|
||||
return (doc && doc.info) || {};
|
||||
return (doc.info || {}) as NotificationInfo;
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
let doc = await AuthModel.findOne({ type: payload.type });
|
||||
private async updateAuthDb(payload: SystemInfo): Promise<any> {
|
||||
let doc = await SystemModel.findOne({ type: payload.type });
|
||||
if (doc) {
|
||||
const updateResult = await AuthModel.update(payload, {
|
||||
const updateResult = await SystemModel.update(payload, {
|
||||
where: { id: doc.id },
|
||||
returning: true,
|
||||
});
|
||||
doc = updateResult[1][0];
|
||||
} else {
|
||||
doc = await AuthModel.create(payload, { returning: true });
|
||||
doc = await SystemModel.create(payload, { returning: true });
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<any> {
|
||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as any);
|
||||
public async getDb(query: any): Promise<SystemInfo> {
|
||||
const doc = await SystemModel.findOne({ where: { ...query } });
|
||||
if (!doc) {
|
||||
throw new Error(`${JSON.stringify(query)} not found`);
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||
|
||||
+100
-15
@@ -1,36 +1,121 @@
|
||||
import pLimit from "p-limit";
|
||||
import PQueue, { QueueAddOptions } from 'p-queue-cjs';
|
||||
import os from 'os';
|
||||
import { AuthDataType, AuthModel } from "../data/auth";
|
||||
import { AuthDataType, SystemModel } from '../data/system';
|
||||
import Logger from '../loaders/logger';
|
||||
import { Dependence } from '../data/dependence';
|
||||
|
||||
interface IDependencyFn<T> {
|
||||
(): Promise<T>;
|
||||
dependency?: Dependence;
|
||||
}
|
||||
class TaskLimit {
|
||||
private oneLimit = pLimit(1);
|
||||
private cpuLimit = pLimit(Math.max(os.cpus().length, 4));
|
||||
private dependenyLimit = new PQueue({ concurrency: 1 });
|
||||
private queuedDependencyIds = new Set<number>([]);
|
||||
private updateLogLimit = new PQueue({ concurrency: 1 });
|
||||
private cronLimit = new PQueue({
|
||||
concurrency: Math.max(os.cpus().length, 4),
|
||||
});
|
||||
private manualCronoLimit = new PQueue({
|
||||
concurrency: Math.max(os.cpus().length, 4),
|
||||
});
|
||||
|
||||
get cronLimitActiveCount() {
|
||||
return this.cronLimit.pending;
|
||||
}
|
||||
|
||||
get cronLimitPendingCount() {
|
||||
return this.cronLimit.size;
|
||||
}
|
||||
|
||||
get firstDependencyId() {
|
||||
return [...this.queuedDependencyIds.values()][0];
|
||||
}
|
||||
|
||||
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 removeQueuedDependency(dependency: Dependence) {
|
||||
if (this.queuedDependencyIds.has(dependency.id!)) {
|
||||
this.queuedDependencyIds.delete(dependency.id!);
|
||||
}
|
||||
}
|
||||
|
||||
public async setCustomLimit(limit?: number) {
|
||||
if (limit) {
|
||||
this.cpuLimit = pLimit(limit);
|
||||
this.cronLimit.concurrency = limit;
|
||||
this.manualCronoLimit.concurrency = limit;
|
||||
return;
|
||||
}
|
||||
const doc = await AuthModel.findOne({ where: { type: AuthDataType.systemConfig } });
|
||||
await SystemModel.sync();
|
||||
const doc = await SystemModel.findOne({
|
||||
where: { type: AuthDataType.systemConfig },
|
||||
});
|
||||
if (doc?.info?.cronConcurrency) {
|
||||
this.cpuLimit = pLimit(doc?.info?.cronConcurrency);
|
||||
this.cronLimit.concurrency = doc.info.cronConcurrency;
|
||||
this.manualCronoLimit.concurrency = doc.info.cronConcurrency;
|
||||
}
|
||||
}
|
||||
|
||||
public runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> {
|
||||
return this.cpuLimit(() => {
|
||||
return fn();
|
||||
});
|
||||
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>): Promise<T> {
|
||||
return this.oneLimit(() => {
|
||||
return fn();
|
||||
});
|
||||
public async manualRunWithCronLimit<T>(
|
||||
fn: () => Promise<T>,
|
||||
options?: Partial<QueueAddOptions>,
|
||||
): Promise<T | void> {
|
||||
return this.manualCronoLimit.add(fn, options);
|
||||
}
|
||||
|
||||
public runDependeny<T>(
|
||||
dependency: Dependence,
|
||||
fn: IDependencyFn<T>,
|
||||
options?: Partial<QueueAddOptions>,
|
||||
): Promise<T | void> {
|
||||
this.queuedDependencyIds.add(dependency.id!);
|
||||
fn.dependency = dependency;
|
||||
return this.dependenyLimit.add(fn, options);
|
||||
}
|
||||
|
||||
public updateDepLog<T>(
|
||||
fn: () => Promise<T>,
|
||||
options?: Partial<QueueAddOptions>,
|
||||
): Promise<T | void> {
|
||||
return this.updateLogLimit.add(fn, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-15
@@ -1,37 +1,31 @@
|
||||
import { spawn } from 'cross-spawn';
|
||||
import taskLimit from "./pLimit";
|
||||
import taskLimit from './pLimit';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
export function runCron(cmd: string): Promise<number> {
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
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.silly('运行命令: ' + cmd);
|
||||
|
||||
Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, command: cmd })}`);
|
||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
Logger.info(
|
||||
'[执行任务失败] %s,时间:%s, 错误信息:%j',
|
||||
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
||||
cmd,
|
||||
new Date().toLocaleString(),
|
||||
data.toString(),
|
||||
);
|
||||
});
|
||||
cp.on('error', (err) => {
|
||||
Logger.error(
|
||||
'[创建任务失败] %s,时间:%s, 错误信息:%j',
|
||||
'[schedule][创建任务失败] 命令: %s, 错误信息: %j',
|
||||
cmd,
|
||||
new Date().toLocaleString(),
|
||||
err,
|
||||
);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
Logger.info(
|
||||
`[任务退出] ${cmd} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
||||
);
|
||||
resolve();
|
||||
cp.on('exit', async (code) => {
|
||||
resolve({ ...options, command: cmd, pid: cp.pid, code });
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'reflect-metadata'; // We need this in order to use @Decorators
|
||||
import config from './config';
|
||||
import express from 'express';
|
||||
import depInjectorLoader from './loaders/depInjector';
|
||||
import Logger from './loaders/logger';
|
||||
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
depInjectorLoader();
|
||||
|
||||
await require('./loaders/update').default({ app });
|
||||
|
||||
app
|
||||
.listen(config.updatePort, '0.0.0.0', () => {
|
||||
Logger.debug(`✌️ 更新服务启动成功!`);
|
||||
console.debug(`✌️ 更新服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
Executable
+274
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env zx
|
||||
import path from 'path';
|
||||
|
||||
const dir_root = process.env.QL_DIR;
|
||||
const file_auth_token = path.join(dir_root, 'static/auth.json');
|
||||
const token_file = path.join(dir_root, 'static/build/token.js');
|
||||
|
||||
let token;
|
||||
|
||||
const createToken = async () => {
|
||||
let tokenCommand = `tsx ${dir_root}/back/token.ts`;
|
||||
if (await fs.exists(token_file)) {
|
||||
tokenCommand = `node ${token_file}`;
|
||||
}
|
||||
token = (await $([tokenCommand])).stdout.trim();
|
||||
};
|
||||
|
||||
const getToken = async () => {
|
||||
if (fs.existsSync(file_auth_token)) {
|
||||
const authTokenData = JSON.parse(fs.readFileSync(file_auth_token, 'utf8'));
|
||||
token = authTokenData.value;
|
||||
const expiration = authTokenData.expiration;
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
if (currentTimeStamp >= expiration) {
|
||||
await createToken();
|
||||
}
|
||||
} else {
|
||||
await createToken();
|
||||
}
|
||||
};
|
||||
|
||||
export const addCronApi = async (schedule, command, name, subId = null) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
name,
|
||||
command,
|
||||
schedule,
|
||||
sub_id: subId,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log(`${name} -> 添加成功`);
|
||||
} else {
|
||||
console.log(`${name} -> 添加失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${name} -> 添加失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCronApi = async (schedule, command, name, id) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
name,
|
||||
command,
|
||||
schedule,
|
||||
id,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log(`${name} -> 更新成功`);
|
||||
} else {
|
||||
console.log(`${name} -> 更新失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${name} -> 更新失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCronCommandApi = async (command, id) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
command,
|
||||
id,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log(`${command} -> 更新成功`);
|
||||
} else {
|
||||
console.log(`${command} -> 更新失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${command} -> 更新失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const delCronApi = async (ids) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(ids),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log('成功');
|
||||
} else {
|
||||
console.log(`失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`删除失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCron = async (
|
||||
ids,
|
||||
status,
|
||||
pid,
|
||||
logPath,
|
||||
lastExecutingTime = 0,
|
||||
runningTime = 0,
|
||||
) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
ids,
|
||||
status,
|
||||
pid,
|
||||
log_path: logPath,
|
||||
last_execution_time: lastExecutingTime,
|
||||
last_running_time: runningTime,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons/status?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code !== 200) {
|
||||
console.log(`更新任务状态失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`更新任务状态失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const notifyApi = async (title, content) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
title,
|
||||
content,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/system/notify?t=${currentTimeStamp}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { code, message } = responseData;
|
||||
if (code === 200) {
|
||||
console.log('通知发送成功🎉');
|
||||
} else {
|
||||
console.log(`通知失败(${message})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`通知失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const findCronApi = async (params) => {
|
||||
const currentTimeStamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://0.0.0.0:5600/open/crons/detail?${params}&t=${currentTimeStamp}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const { data } = responseData;
|
||||
if (data === 'null') {
|
||||
console.log('');
|
||||
} else {
|
||||
const { name } = data;
|
||||
console.log(name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`查找失败(${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
await getToken();
|
||||
@@ -1,13 +0,0 @@
|
||||
import { CommandModule } from 'yargs';
|
||||
export const updateCommand: CommandModule = {
|
||||
command: 'update',
|
||||
describe: 'Update and restart qinglong',
|
||||
builder: (yargs) => {
|
||||
return yargs.option('repositority', {
|
||||
type: 'string',
|
||||
alias: 'r',
|
||||
describe: `Specify the release warehouse address of the package`,
|
||||
});
|
||||
},
|
||||
handler: async (argv) => {},
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
import 'zx/globals';
|
||||
let initialVars = [];
|
||||
|
||||
export const storeEnvVars = async () => {
|
||||
const stdout = (await $`env`).lines();
|
||||
initialVars = stdout.map((line) => line.split('=')[0]);
|
||||
};
|
||||
|
||||
export const restoreEnvVars = async () => {
|
||||
const stdout = (await $`env`).lines();
|
||||
const currentVars = stdout.map((line) => line.split('=')[0]);
|
||||
|
||||
for (const key of currentVars) {
|
||||
if (!initialVars.includes(key)) {
|
||||
await $`unset ${key}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await storeEnvVars();
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as yargs from 'yargs';
|
||||
import { green, red } from 'chalk';
|
||||
import { updateCommand } from './commands/update';
|
||||
|
||||
yargs
|
||||
.usage('Usage: ql [command] <options>')
|
||||
.command(updateCommand)
|
||||
.fail((err) => {
|
||||
console.error(`${red(err)}`);
|
||||
})
|
||||
.alias('h', 'help')
|
||||
.showHelp()
|
||||
.recommendCommands().argv;
|
||||
Executable
+302
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
import {
|
||||
dirScripts,
|
||||
dirLog,
|
||||
handleTaskStart,
|
||||
runTaskBefore,
|
||||
runTaskAfter,
|
||||
handleTaskEnd,
|
||||
globalState,
|
||||
formatLogTime,
|
||||
} from './share.mjs';
|
||||
import { basename, dirname } from 'path';
|
||||
|
||||
function defineProgram(fileParam) {
|
||||
if (fileParam.endsWith('.js') || fileParam.endsWith('.mjs')) {
|
||||
return 'node';
|
||||
} else if (fileParam.endsWith('.py') || fileParam.endsWith('.pyc')) {
|
||||
return 'python3';
|
||||
} else if (fileParam.endsWith('.sh')) {
|
||||
return 'bash';
|
||||
} else if (fileParam.endsWith('.ts')) {
|
||||
return $`command -v tsx`
|
||||
.then(() => 'tsx')
|
||||
.catch(() => 'ts-node-transpile-only');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function randomDelay(fileParam) {
|
||||
const randomDelayMax = process.env.RandomDelay;
|
||||
if (randomDelayMax && randomDelayMax > 0) {
|
||||
const fileExtensions = process.env.RandomDelayFileExtensions || 'js';
|
||||
const ignoredMinutes = process.env.RandomDelayIgnoredMinutes || '0 30';
|
||||
const currentMin = new Date().getMinutes();
|
||||
|
||||
if (
|
||||
fileExtensions.split(' ').some((ext) => fileParam.endsWith(`.${ext}`))
|
||||
) {
|
||||
if (
|
||||
ignoredMinutes.split(' ').some((min) => parseInt(min) === currentMin)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const delaySecond = Math.floor(Math.random() * randomDelayMax) + 1;
|
||||
console.log(
|
||||
`任务随机延迟 ${delaySecond} 秒,配置文件参数 RandomDelay 置空可取消延迟`,
|
||||
);
|
||||
await sleep(delaySecond * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function genArrayScripts() {
|
||||
const arrayScripts = [];
|
||||
const arrayScriptsName = [];
|
||||
const files = await fs.readdir(dirScripts);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.js') && file !== 'sendNotify.js') {
|
||||
arrayScripts.push(file);
|
||||
const content = await fs.readFile(`${dirScripts}/${file}`, 'utf8');
|
||||
const match = content.match(/new Env\(['"]([^'"]+)['"]\)/);
|
||||
arrayScriptsName.push(match ? match[1] : '<未识别出活动名称>');
|
||||
}
|
||||
}
|
||||
|
||||
return { arrayScripts, arrayScriptsName };
|
||||
}
|
||||
|
||||
async function usage() {
|
||||
const { arrayScripts, arrayScriptsName } = await genArrayScripts();
|
||||
console.log(
|
||||
`task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “${dirScripts}/” 目录后的相对路径(定时任务中请写作相对路径),用法为:`,
|
||||
);
|
||||
console.log(
|
||||
`1.$cmdTask <fileName> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数`,
|
||||
);
|
||||
console.log(
|
||||
`2.$cmdTask <fileName> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中`,
|
||||
);
|
||||
console.log(
|
||||
`3.$cmdTask <fileName> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行`,
|
||||
);
|
||||
console.log(
|
||||
`4.$cmdTask <fileName> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行`,
|
||||
);
|
||||
if (arrayScripts.length > 0) {
|
||||
console.log(`\n当前有以下脚本可以运行:`);
|
||||
arrayScripts.forEach((script, i) =>
|
||||
console.log(`${i + 1}. ${arrayScriptsName[i]}:${script}`),
|
||||
);
|
||||
} else {
|
||||
console.log(`\n暂无脚本可以执行`);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseDuration(d) {
|
||||
if (typeof d == 'number') {
|
||||
if (isNaN(d) || d < 0) throw new Error(`Invalid duration: "${d}".`);
|
||||
return d;
|
||||
} else if (/\d+s/.test(d)) {
|
||||
return +d.slice(0, -1) * 1000;
|
||||
} else if (/\d+ms/.test(d)) {
|
||||
return +d.slice(0, -2);
|
||||
} else if (/\d+m/.test(d)) {
|
||||
return +d.slice(0, -1) * 1000 * 60;
|
||||
} else if (/\d+h/.test(d)) {
|
||||
return +d.slice(0, -1) * 1000 * 60 * 60;
|
||||
} else if (/\d+d/.test(d)) {
|
||||
return +d.slice(0, -1) * 1000 * 60 * 60 * 24;
|
||||
}
|
||||
throw new Error(`Unknown duration: "${d}".`);
|
||||
}
|
||||
|
||||
async function runWithTimeout(command) {
|
||||
if (globalState.commandTimeoutTime) {
|
||||
const timeoutNumber = parseDuration(globalState.commandTimeoutTime);
|
||||
await $([command]).timeout(timeoutNumber).nothrow().pipe(process.stdout);
|
||||
} else {
|
||||
await $([command]).nothrow().pipe(process.stdout);
|
||||
}
|
||||
}
|
||||
|
||||
async function runNormal(fileParam, scriptParams) {
|
||||
if (
|
||||
!scriptParams.includes('now') &&
|
||||
process.env.realTime !== 'true' &&
|
||||
process.env.noDelay !== 'true'
|
||||
) {
|
||||
await randomDelay(fileParam);
|
||||
}
|
||||
cd(dirScripts);
|
||||
const relativePath = dirname(fileParam);
|
||||
if (!fileParam.startsWith('/') && relativePath) {
|
||||
cd(relativePath);
|
||||
fileParam = fileParam.replace(`${relativePath}/`, '');
|
||||
}
|
||||
await runWithTimeout(
|
||||
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function runConcurrent(fileParam, envParam, numParam, scriptParams) {
|
||||
if (!envParam || !numParam) {
|
||||
console.log(`缺少并发运行的环境变量参数 task xxx.js conc Test 1 3`);
|
||||
return;
|
||||
}
|
||||
|
||||
const array = (process.env[envParam] || '').split('&');
|
||||
const runArr = expandRange(numParam, array.length);
|
||||
const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean);
|
||||
|
||||
const singleLogTime = formatLogTime(new Date());
|
||||
|
||||
cd(dirScripts);
|
||||
const relativePath = dirname(fileParam);
|
||||
if (relativePath && fileParam.includes('/')) {
|
||||
cd(relativePath);
|
||||
fileParam = fileParam.replace(`${relativePath}/`, '');
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
arrayRun.map(async (env, i) => {
|
||||
const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${
|
||||
i + 1
|
||||
}.log`;
|
||||
await runWithTimeout(
|
||||
`${envParam}="${env.replace('"', '\\"')}" ${
|
||||
globalState.whichProgram
|
||||
} ${fileParam} ${scriptParams} &>${singleLogPath}`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
for (let i = 0; i < arrayRun.length; i++) {
|
||||
const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${
|
||||
i + 1
|
||||
}.log`;
|
||||
const log = await fs.readFile(singleLogPath, 'utf8');
|
||||
console.log(log);
|
||||
await fs.unlink(singleLogPath);
|
||||
}
|
||||
}
|
||||
|
||||
async function runDesignated(fileParam, envParam, numParam, scriptParams) {
|
||||
if (!envParam || !numParam) {
|
||||
console.log(`缺少单独运行的参数 task xxx.js desi Test 1 3`);
|
||||
return;
|
||||
}
|
||||
|
||||
const array = (process.env[envParam] || '').split('&');
|
||||
const runArr = expandRange(numParam, array.length);
|
||||
const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean);
|
||||
const cookieStr = arrayRun.join('&');
|
||||
cd(dirScripts);
|
||||
const relativePath = dirname(fileParam);
|
||||
if (relativePath && fileParam.includes('/')) {
|
||||
cd(relativePath);
|
||||
fileParam = fileParam.replace(`${relativePath}/`, '');
|
||||
}
|
||||
|
||||
console.log('cookieStr', cookieStr.length, arrayRun.length);
|
||||
// ${envParam}="${cookieStr.replace('"', '\\"')}"
|
||||
await runWithTimeout(
|
||||
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function runElse(fileParam, scriptParams) {
|
||||
cd(dirScripts);
|
||||
const relativePath = dirname(fileParam);
|
||||
if (relativePath && fileParam.includes('/')) {
|
||||
cd(relativePath);
|
||||
fileParam = fileParam.replace(`${relativePath}/`, './');
|
||||
}
|
||||
|
||||
await runWithTimeout(
|
||||
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
|
||||
);
|
||||
}
|
||||
|
||||
function expandRange(rangeStr, max) {
|
||||
const tempRangeStr = rangeStr
|
||||
.replace(/-max/g, `-${max}`)
|
||||
.replace(/max-/g, `${max}-`);
|
||||
|
||||
return tempRangeStr.split(' ').flatMap((part) => {
|
||||
const rangeMatch = part.match(/^(\d+)([-~_])(\d+)$/);
|
||||
if (rangeMatch) {
|
||||
const [, start, , end] = rangeMatch.map(Number);
|
||||
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||
}
|
||||
return Number(part);
|
||||
});
|
||||
}
|
||||
|
||||
async function main(taskShellParams, scriptParams) {
|
||||
const [fileParam, action, envParam, ...others] = taskShellParams;
|
||||
if (taskShellParams.length === 0) {
|
||||
return await usage();
|
||||
}
|
||||
if (fileParam && /\.(js|py|pyc|sh|ts)$/.test(fileParam)) {
|
||||
const filePath = fileParam.startsWith('/')
|
||||
? fileParam
|
||||
: path.join(dirScripts, fileParam);
|
||||
if (!(await fs.exists(filePath))) {
|
||||
console.log(`文件不存在 ${fileParam}`);
|
||||
return;
|
||||
}
|
||||
switch (action) {
|
||||
case undefined:
|
||||
return await runNormal(fileParam, scriptParams);
|
||||
case 'now':
|
||||
return await runNormal(fileParam, scriptParams);
|
||||
case 'conc':
|
||||
return await runConcurrent(
|
||||
fileParam,
|
||||
envParam,
|
||||
others.join(' '),
|
||||
scriptParams,
|
||||
);
|
||||
case 'desi':
|
||||
return await runDesignated(
|
||||
fileParam,
|
||||
envParam,
|
||||
others.join(' '),
|
||||
scriptParams,
|
||||
);
|
||||
}
|
||||
}
|
||||
await runElse(fileParam, taskShellParams.slice(1).concat(scriptParams));
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const taskArgv = minimist(process.argv.slice(3), {
|
||||
'--': true,
|
||||
});
|
||||
const { _: taskShellParams, m, '--': scriptParams, GlobalState } = taskArgv;
|
||||
const cacheState = JSON.parse(GlobalState || '{}');
|
||||
for (const key in cacheState) {
|
||||
globalState[key] = cacheState[key];
|
||||
}
|
||||
if (m) {
|
||||
globalState.commandTimeoutTime = m;
|
||||
}
|
||||
globalState.whichProgram = await defineProgram(taskShellParams[0]);
|
||||
|
||||
await handleTaskStart();
|
||||
await runTaskBefore();
|
||||
await main(taskShellParams, scriptParams);
|
||||
await runTaskAfter();
|
||||
await handleTaskEnd();
|
||||
}
|
||||
|
||||
async function singleHandle() {}
|
||||
|
||||
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP'];
|
||||
signals.forEach((sig) => process.on(sig, singleHandle));
|
||||
|
||||
await run();
|
||||
Executable
+500
@@ -0,0 +1,500 @@
|
||||
#!/usr/bin/env zx
|
||||
import 'zx/globals';
|
||||
// $.verbose = true;
|
||||
|
||||
import { updateCron, notifyApi } from './api.mjs';
|
||||
import { restoreEnvVars } from './env.mjs';
|
||||
|
||||
export const dirRoot = process.env.QL_DIR;
|
||||
export const dirTmp = path.join(dirRoot, '.tmp');
|
||||
export const dirData = process.env.QL_DATA_DIR
|
||||
? process.env.QL_DATA_DIR.endsWith('/')
|
||||
? process.env.QL_DATA_DIR.slice(-1)
|
||||
: process.env.QL_DATA_DIR
|
||||
: path.join(dirRoot, 'data');
|
||||
export const dirShell = path.join(dirRoot, 'shell');
|
||||
export const dirCli = path.join(dirRoot, 'cli');
|
||||
export const dirSample = path.join(dirRoot, 'sample');
|
||||
export const dirStatic = path.join(dirRoot, 'static');
|
||||
export const dirConfig = path.join(dirData, 'config');
|
||||
export const dirScripts = path.join(dirData, 'scripts');
|
||||
export const dirRepo = path.join(dirData, 'repo');
|
||||
export const dirRaw = path.join(dirData, 'raw');
|
||||
export const dirLog = path.join(dirData, 'log');
|
||||
export const dirDb = path.join(dirData, 'db');
|
||||
export const dirDep = path.join(dirData, 'deps');
|
||||
export const dirListTmp = path.join(dirLog, '.tmp');
|
||||
export const dirUpdateLog = path.join(dirLog, 'update');
|
||||
export const qlStaticRepo = path.join(dirRepo, 'static');
|
||||
|
||||
export const fileConfigSample = path.join(dirSample, 'config.sample.sh');
|
||||
export const fileEnv = path.join(dirConfig, 'env.sh');
|
||||
export const jsFileEnv = path.join(dirConfig, 'env.js');
|
||||
export const fileConfigUser = path.join(dirConfig, 'config.sh');
|
||||
export const fileAuthSample = path.join(dirSample, 'auth.sample.json');
|
||||
export const fileAuthUser = path.join(dirConfig, 'auth.json');
|
||||
export const fileAuthToken = path.join(dirConfig, 'token.json');
|
||||
export const fileExtraShell = path.join(dirConfig, 'extra.sh');
|
||||
export const fileTaskBefore = path.join(dirConfig, 'task_before.sh');
|
||||
export const fileTaskAfter = path.join(dirConfig, 'task_after.sh');
|
||||
export const fileTaskSample = path.join(dirSample, 'task.sample.sh');
|
||||
export const fileExtraSample = path.join(dirSample, 'extra.sample.sh');
|
||||
export const fileNotifyJsSample = path.join(dirSample, 'notify.js');
|
||||
export const fileNotifyPySample = path.join(dirSample, 'notify.py');
|
||||
export const fileTestJsSample = path.join(dirSample, 'ql_sample.js');
|
||||
export const fileTestPySample = path.join(dirSample, 'ql_sample.py');
|
||||
export const fileNotifyPy = path.join(dirScripts, 'notify.py');
|
||||
export const fileNotifyJs = path.join(dirScripts, 'sendNotify.js');
|
||||
export const fileTestJs = path.join(dirScripts, 'ql_sample.js');
|
||||
export const fileTestPy = path.join(dirScripts, 'ql_sample.py');
|
||||
export const nginxAppConf = path.join(dirRoot, 'docker/front.conf');
|
||||
export const nginxConf = path.join(dirRoot, 'docker/nginx.conf');
|
||||
export const depNotifyPy = path.join(dirDep, 'notify.py');
|
||||
export const depNotifyJs = path.join(dirDep, 'sendNotify.js');
|
||||
|
||||
export const listCrontabUser = path.join(dirConfig, 'crontab.list');
|
||||
export const listCrontabSample = path.join(dirSample, 'crontab.sample.list');
|
||||
export const listOwnScripts = path.join(dirListTmp, 'own_scripts.list');
|
||||
export const listOwnUser = path.join(dirListTmp, 'own_user.list');
|
||||
export const listOwnAdd = path.join(dirListTmp, 'own_add.list');
|
||||
export const listOwnDrop = path.join(dirListTmp, 'own_drop.list');
|
||||
|
||||
export const globalState = {};
|
||||
|
||||
export const initEnv = () => {
|
||||
$.prefix +=
|
||||
'export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules;';
|
||||
$.prefix += 'export PYTHONUNBUFFERED=1;';
|
||||
$.prefix += 'export TERM=xterm-color;';
|
||||
};
|
||||
|
||||
export const importConfig = async () => {
|
||||
if (await fs.exists(fileConfigUser)) {
|
||||
$.prefix += (await fs.readFile(fileConfigUser, 'utf8'));
|
||||
}
|
||||
// if (process.env.LOAD_ENV !== 'false' && (await fs.exists(fileEnv))) {
|
||||
// $.prefix += (await fs.readFile(fileEnv, 'utf8'));
|
||||
// }
|
||||
require(jsFileEnv)
|
||||
|
||||
globalState.qlBaseUrl = process.env.QlBaseUrl || '/';
|
||||
globalState.qlPort = process.env.QlPort || '5700';
|
||||
globalState.commandTimeoutTime = process.env.CommandTimeoutTime;
|
||||
globalState.fileExtensions = process.env.RepoFileExtensions || 'js py';
|
||||
globalState.proxyUrl = process.env.ProxyUrl || '';
|
||||
globalState.currentBranch = process.env.QL_BRANCH;
|
||||
|
||||
if (process.env.DefaultCronRule) {
|
||||
globalState.defaultCron = process.env.DefaultCronRule;
|
||||
} else {
|
||||
globalState.defaultCron = `${Math.floor(Math.random() * 60)} ${Math.floor(
|
||||
Math.random() * 24,
|
||||
)} * * *`;
|
||||
}
|
||||
|
||||
globalState.cpuWarn = process.env.CpuWarn;
|
||||
globalState.memWarn = process.env.MemoryWarn;
|
||||
globalState.diskWarn = process.env.DiskWarn;
|
||||
};
|
||||
|
||||
export const setProxy = (proxy) => {
|
||||
if (proxy) {
|
||||
globalState.proxyUrl = proxy;
|
||||
}
|
||||
if (globalState.proxyUrl) {
|
||||
$`export http_proxy=${globalState.proxyUrl}`;
|
||||
$`export https_proxy=${globalState.proxyUrl}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const unsetProxy = () => {
|
||||
$`unset http_proxy`;
|
||||
$`unset https_proxy`;
|
||||
};
|
||||
|
||||
export const makeDir = async (dir) => {
|
||||
if (!(await fs.exists(dir))) {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
export const detectTermux = () => {
|
||||
globalState.isTermux = process.env.PATH?.includes('com.termux') ? 1 : 0;
|
||||
};
|
||||
|
||||
export const detectMacos = () => {
|
||||
globalState.isMacos = os.type() === 'Darwin' ? 1 : 0;
|
||||
};
|
||||
|
||||
export const genRandomNum = (number) => {
|
||||
return Math.floor(Math.random() * number);
|
||||
};
|
||||
|
||||
export const fixConfig = async () => {
|
||||
await makeDir(dirTmp);
|
||||
await makeDir(dirStatic);
|
||||
await makeDir(dirData);
|
||||
await makeDir(dirConfig);
|
||||
await makeDir(dirLog);
|
||||
await makeDir(dirDb);
|
||||
await makeDir(dirScripts);
|
||||
await makeDir(dirListTmp);
|
||||
await makeDir(dirRepo);
|
||||
await makeDir(dirRaw);
|
||||
await makeDir(dirUpdateLog);
|
||||
await makeDir(dirDep);
|
||||
|
||||
if (!(await fs.exists(fileConfigUser))) {
|
||||
console.log(
|
||||
`复制一份 ${fileConfigSample} 为 ${fileConfigUser},随后请按注释编辑你的配置文件:${fileConfigUser}`,
|
||||
);
|
||||
await fs.copyFile(fileConfigSample, fileConfigUser);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileEnv))) {
|
||||
console.log(
|
||||
'检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...',
|
||||
);
|
||||
await fs.writeFile(fileEnv, '');
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileTaskBefore))) {
|
||||
console.log(`复制一份 ${fileTaskSample} 为 ${fileTaskBefore}`);
|
||||
await fs.copyFile(fileTaskSample, fileTaskBefore);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileTaskAfter))) {
|
||||
console.log(`复制一份 ${fileTaskSample} 为 ${fileTaskAfter}`);
|
||||
await fs.copyFile(fileTaskSample, fileTaskAfter);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileExtraShell))) {
|
||||
console.log(`复制一份 ${fileExtraSample} 为 ${fileExtraShell}`);
|
||||
await fs.copyFile(fileExtraSample, fileExtraShell);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileAuthUser))) {
|
||||
console.log(`复制一份 ${fileAuthSample} 为 ${fileAuthUser}`);
|
||||
await fs.copyFile(fileAuthSample, fileAuthUser);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileNotifyPy))) {
|
||||
console.log(`复制一份 ${fileNotifyPySample} 为 ${fileNotifyPy}`);
|
||||
await fs.copyFile(fileNotifyPySample, fileNotifyPy);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileNotifyJs))) {
|
||||
console.log(`复制一份 ${fileNotifyJsSample} 为 ${fileNotifyJs}`);
|
||||
await fs.copyFile(fileNotifyJsSample, fileNotifyJs);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileTestJs))) {
|
||||
await fs.copyFile(fileTestJsSample, fileTestJs);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(fileTestPy))) {
|
||||
await fs.copyFile(fileTestPySample, fileTestPy);
|
||||
}
|
||||
|
||||
if (await fs.exists('/etc/nginx/conf.d/default.conf')) {
|
||||
console.log('检测到你可能未修改过默认nginx配置,将帮你删除');
|
||||
await fs.unlink('/etc/nginx/conf.d/default.conf');
|
||||
}
|
||||
if (!(await fs.exists(depNotifyJs))) {
|
||||
console.log(`复制一份 ${fileNotifyJsSample} 为 ${depNotifyJs}`);
|
||||
await fs.copyFile(fileNotifyJsSample, depNotifyJs);
|
||||
}
|
||||
|
||||
if (!(await fs.exists(depNotifyPy))) {
|
||||
console.log(`复制一份 ${fileNotifyPySample} 为 ${depNotifyPy}`);
|
||||
await fs.copyFile(fileNotifyPySample, depNotifyPy);
|
||||
}
|
||||
};
|
||||
|
||||
export const npmInstallSub = async () => {
|
||||
if (globalState.isTermux === 1) {
|
||||
await $`npm install --production --no-bin-links`;
|
||||
} else if (!(await $`command -v pnpm`)) {
|
||||
await $`npm install --production`;
|
||||
} else {
|
||||
await $`pnpm install --loglevel error --production`;
|
||||
}
|
||||
};
|
||||
|
||||
export const npmInstall = async (dirWork) => {
|
||||
const dirCurrent = process.cwd();
|
||||
|
||||
await $`cd ${dirWork}`;
|
||||
console.log(`安装 ${dirWork} 依赖包...`);
|
||||
await npmInstallSub();
|
||||
await $`cd ${dirCurrent}`;
|
||||
};
|
||||
|
||||
export const diffAndCopy = async (copySource, copyTo) => {
|
||||
if (
|
||||
!(await fs.exists(copyTo)) ||
|
||||
(await $`diff ${copySource} ${copyTo}`).exitCode !== 0
|
||||
) {
|
||||
await fs.copyFile(copySource, copyTo);
|
||||
}
|
||||
};
|
||||
|
||||
export const gitCloneScripts = async (url, dir, branch, proxy) => {
|
||||
const partCmd = branch ? `-b ${branch}` : '';
|
||||
console.log(`开始拉取仓库 ${globalState.uniqPath} 到 ${dir}`);
|
||||
|
||||
setProxy(proxy);
|
||||
|
||||
const res = await $`git clone -q --depth=1 ${partCmd} ${url} ${dir}`;
|
||||
globalState.exitStatus = res.exitCode;
|
||||
|
||||
unsetProxy();
|
||||
};
|
||||
|
||||
export const randomRange = (begin, end) => {
|
||||
return Math.floor(Math.random() * (end - begin) + begin);
|
||||
};
|
||||
|
||||
export const deletePm2 = async () => {
|
||||
await $`cd ${dirRoot}`;
|
||||
await $`pm2 delete ecosystem.config.js`;
|
||||
};
|
||||
|
||||
export const reloadPm2 = async () => {
|
||||
await $`cd ${dirRoot}`;
|
||||
restoreEnvVars();
|
||||
await $`pm2 flush &>/dev/null`;
|
||||
await $`pm2 startOrGracefulReload ecosystem.config.js`;
|
||||
};
|
||||
|
||||
export const reloadUpdate = async () => {
|
||||
await $`cd ${dirRoot}`;
|
||||
restoreEnvVars();
|
||||
await $`pm2 flush &>/dev/null`;
|
||||
await $`pm2 startOrGracefulReload other.config.js`;
|
||||
};
|
||||
|
||||
export const diffTime = (beginTime, endTime) => {
|
||||
let diffTime;
|
||||
if (globalState.isMacos === 1) {
|
||||
diffTime = (+new Date(endTime) - +new Date(beginTime)) / 1000;
|
||||
} else {
|
||||
diffTime =
|
||||
(new Date(endTime).getTime() - new Date(beginTime).getTime()) / 1000;
|
||||
}
|
||||
return diffTime;
|
||||
};
|
||||
|
||||
export const formatTime = (time) => {
|
||||
// 秒
|
||||
return new Date(time).toLocaleString();
|
||||
};
|
||||
|
||||
function pad(n, min = 10) {
|
||||
return n < min ? '0' + n : n;
|
||||
}
|
||||
|
||||
export function formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const day = pad(date.getDate());
|
||||
const hour = pad(date.getHours());
|
||||
const minute = pad(date.getMinutes());
|
||||
const second = pad(date.getSeconds());
|
||||
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
export const formatLogTime = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const day = pad(date.getDate());
|
||||
const hour = pad(date.getHours());
|
||||
const minute = pad(date.getMinutes());
|
||||
const second = pad(date.getSeconds());
|
||||
const milliSecond = pad(date.getMilliseconds(), 100);
|
||||
|
||||
return `${year}-${month}-${day}-${hour}-${minute}-${second}-${milliSecond}`;
|
||||
};
|
||||
|
||||
export const formatTimestamp = (date) => {
|
||||
return Math.floor(date.getTime() / 1000);
|
||||
};
|
||||
|
||||
export const patchVersion = async () => {
|
||||
await $`git config --global pull.rebase false`;
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'db/cookie.db'))) {
|
||||
console.log('检测到旧的db文件,拷贝为新db...');
|
||||
await $`mv ${path.join(dirRoot, 'db/cookie.db')} ${path.join(
|
||||
dirRoot,
|
||||
'db/env.db',
|
||||
)}`;
|
||||
await $`rm -rf ${path.join(dirRoot, 'db/cookie.db')}`;
|
||||
}
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'db'))) {
|
||||
console.log('检测到旧的db目录,拷贝到data目录...');
|
||||
await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`;
|
||||
}
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'scripts'))) {
|
||||
console.log('检测到旧的scripts目录,拷贝到data目录...');
|
||||
await $`cp -rf ${path.join(dirRoot, 'scripts')} ${dirData}`;
|
||||
}
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'log'))) {
|
||||
console.log('检测到旧的log目录,拷贝到data目录...');
|
||||
await $`cp -rf ${path.join(dirRoot, 'log')} ${dirData}`;
|
||||
}
|
||||
|
||||
if (await fs.exists(path.join(dirRoot, 'config'))) {
|
||||
console.log('检测到旧的config目录,拷贝到data目录...');
|
||||
await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const initNginx = async () => {
|
||||
await fs.copyFile(nginxConf, '/etc/nginx/nginx.conf');
|
||||
await fs.copyFile(nginxAppConf, '/etc/nginx/conf.d/front.conf');
|
||||
let locationUrl = '/';
|
||||
let aliasStr = '';
|
||||
let rootStr = '';
|
||||
let qlBaseUrl = globalState.qlBaseUrl;
|
||||
let qlPort = globalState.qlPort;
|
||||
if (qlBaseUrl !== '/') {
|
||||
if (!qlBaseUrl.startsWith('/')) {
|
||||
qlBaseUrl = `/${qlBaseUrl}`;
|
||||
}
|
||||
if (!qlBaseUrl.endsWith('/')) {
|
||||
qlBaseUrl = `${qlBaseUrl}/`;
|
||||
}
|
||||
locationUrl = `^~${qlBaseUrl.slice(0, -1)}`;
|
||||
aliasStr = `alias ${path.join(dirStatic, 'dist')};`;
|
||||
const file = await fs.readFile(
|
||||
path.join(dirStatic, 'dist/index.html'),
|
||||
'utf8',
|
||||
);
|
||||
if (!file.includes(`<base href="${qlBaseUrl}">`)) {
|
||||
await fs.writeFile(
|
||||
path.join(dirStatic, 'dist/index.html'),
|
||||
`<base href="${qlBaseUrl}">\n${file}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
rootStr = `root ${path.join(dirStatic, 'dist')};`;
|
||||
}
|
||||
await $`sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf`;
|
||||
await $`sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf`;
|
||||
await $`sed -i "s,QL_BASE_URL_LOCATION,${locationUrl},g" /etc/nginx/conf.d/front.conf`;
|
||||
let ipv6Str = '';
|
||||
const ipv6 = await $`ip a | grep inet6`;
|
||||
if (ipv6.stdout.trim()) {
|
||||
ipv6Str = 'listen [::]:${qlPort} ipv6only=on;';
|
||||
}
|
||||
const ipv4Str = `listen ${qlPort};`;
|
||||
await $`sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf`;
|
||||
await $`sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf`;
|
||||
};
|
||||
|
||||
async function checkServer() {
|
||||
const cpuWarn = parseInt(process.env.cpuWarn || '0');
|
||||
const memWarn = parseInt(process.env.memWarn || '0');
|
||||
const diskWarn = parseInt(process.env.diskWarn || '0');
|
||||
|
||||
if (cpuWarn && memWarn && diskWarn) {
|
||||
const topResult = await $`top -b -n 1`;
|
||||
const cpuUse = parseInt(
|
||||
topResult.stdout.match(/CPU\s+(\d+)\%/)?.[1] || '0',
|
||||
);
|
||||
const memFree = parseInt(
|
||||
(await $`free -m`).stdout.match(/Mem:\s+(\d+)/)?.[1] || '0',
|
||||
);
|
||||
const memTotal = parseInt(
|
||||
(await $`free -m`).stdout.match(/Mem:\s+\d+\s+(\d+)/)?.[1] || '0',
|
||||
);
|
||||
const diskUse = parseInt(
|
||||
(await $`df -P`).stdout.match(/\/dev.*\s+(\d+)\%/)?.[1] || '0',
|
||||
);
|
||||
if (memFree && memTotal && diskUse && cpuUse) {
|
||||
const memUse = Math.floor((memFree * 100) / memTotal);
|
||||
|
||||
if (cpuUse > cpuWarn || memFree < memWarn || diskUse > diskWarn) {
|
||||
const resource = topResult.stdout
|
||||
.split('\n')
|
||||
.slice(7, 17)
|
||||
.map((line) => line.replace(/\s+/g, ' '))
|
||||
.join('\\n');
|
||||
await notifyApi(
|
||||
'服务器资源异常警告',
|
||||
`当前CPU占用 ${cpuUse}% 内存占用 ${memUse}% 磁盘占用 ${diskUse}% \n资源占用详情 \n\n ${resource}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const handleTaskStart = async () => {
|
||||
if (globalState.ID) {
|
||||
await updateCron(
|
||||
[globalState.ID],
|
||||
'0',
|
||||
String(process.pid),
|
||||
globalState.logPath,
|
||||
globalState.beginTimestamp,
|
||||
);
|
||||
}
|
||||
console.log(`## 开始执行... ${globalState.beginTime}\n`);
|
||||
};
|
||||
|
||||
export const runTaskBefore = async () => {
|
||||
if (globalState.isMacos === 0) {
|
||||
await checkServer();
|
||||
}
|
||||
await $`. ${fileTaskBefore} "$@"`;
|
||||
|
||||
if (globalState.taskBefore) {
|
||||
console.log('执行前置命令');
|
||||
await $`eval ${globalState.taskBefore}`;
|
||||
console.log('执行前置命令结束');
|
||||
}
|
||||
};
|
||||
|
||||
export const runTaskAfter = async () => {
|
||||
await $`. ${fileTaskAfter} "$@"`;
|
||||
|
||||
if (globalState.taskAfter) {
|
||||
console.log('执行后置命令');
|
||||
await $`eval "${globalState.taskAfter}"`;
|
||||
console.log('执行后置命令结束');
|
||||
}
|
||||
};
|
||||
|
||||
export const handleTaskEnd = async () => {
|
||||
const etime = new Date();
|
||||
const endTime = formatDate(etime);
|
||||
const endTimestamp = formatTimestamp(etime);
|
||||
let diffTime = endTimestamp - globalState.beginTimestamp;
|
||||
|
||||
if (diffTime === 0) {
|
||||
diffTime = 1;
|
||||
}
|
||||
|
||||
if (globalState.ID) {
|
||||
await updateCron(
|
||||
[globalState.ID],
|
||||
'1',
|
||||
`${process.pid}`,
|
||||
globalState.logPath,
|
||||
globalState.beginTimestamp,
|
||||
diffTime,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`\n## 执行结束... ${endTime} 耗时 ${diffTime} 秒`);
|
||||
};
|
||||
|
||||
initEnv();
|
||||
detectTermux();
|
||||
detectMacos();
|
||||
await importConfig();
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env zx
|
||||
import 'zx/globals';
|
||||
|
||||
import { PassThrough } from 'node:stream';
|
||||
import {
|
||||
listCrontabUser,
|
||||
formatLogTime,
|
||||
formatTimestamp,
|
||||
formatDate,
|
||||
dirLog,
|
||||
dirCli,
|
||||
globalState,
|
||||
handleTaskEnd,
|
||||
} from './share.mjs';
|
||||
import './api.mjs';
|
||||
|
||||
// $.verbose = true;
|
||||
|
||||
async function singleHandle() {}
|
||||
|
||||
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP'];
|
||||
signals.forEach((sig) => process.on(sig, singleHandle));
|
||||
|
||||
export async function handleLogPath(fileParam) {
|
||||
let ID = $.env.ID;
|
||||
if (!ID) {
|
||||
const grepResult =
|
||||
await $`grep -E "task.* ${fileParam}" ${listCrontabUser}`.nothrow();
|
||||
ID = grepResult.stdout.match(/ID=(\d+)/)?.[1];
|
||||
}
|
||||
globalState.ID = ID;
|
||||
|
||||
const suffix = ID && parseInt(ID, 10) > 0 ? `_${ID}` : '';
|
||||
globalState.time = new Date();
|
||||
const logTime = formatLogTime(globalState.time);
|
||||
let logDirTmp = path.basename(fileParam);
|
||||
let logDirTmpPath = '';
|
||||
|
||||
if (fileParam.includes('/')) {
|
||||
logDirTmpPath = path.isAbsolute(fileParam)
|
||||
? fileParam.substring(1)
|
||||
: fileParam;
|
||||
logDirTmpPath = path.basename(path.dirname(logDirTmpPath));
|
||||
}
|
||||
|
||||
if (logDirTmpPath) {
|
||||
logDirTmp = `${logDirTmpPath}_${logDirTmp}`;
|
||||
}
|
||||
|
||||
const logDir = `${logDirTmp.replace(/\.[^/.]+$/, '')}${suffix}`;
|
||||
globalState.logDir = logDir;
|
||||
globalState.logPath = `${logDir}/${logTime}.log`;
|
||||
if ($.env.real_log_path) {
|
||||
globalState.logPath = realLogPath;
|
||||
}
|
||||
|
||||
await $`mkdir -p ${dirLog}/${logDir}`;
|
||||
}
|
||||
|
||||
export function initBeginTime() {
|
||||
globalState.beginTime = formatDate(globalState.time);
|
||||
globalState.beginTimestamp = formatTimestamp(globalState.time);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const taskArgv = minimist(process.argv.slice(3), {
|
||||
'--': true,
|
||||
});
|
||||
const {
|
||||
_: [scriptFile],
|
||||
} = taskArgv;
|
||||
|
||||
await handleLogPath(scriptFile);
|
||||
initBeginTime();
|
||||
|
||||
cd(`${dirCli}`);
|
||||
const logStream = fs.createWriteStream(`${dirLog}/${globalState.logPath}`);
|
||||
const passThrough = new PassThrough();
|
||||
passThrough.pipe(logStream);
|
||||
const p = $`./otask.mjs ${process.argv.slice(
|
||||
3,
|
||||
)} --GlobalState=${JSON.stringify(globalState)} 2>&1`.nothrow();
|
||||
p.stdout.pipe(passThrough).pipe(process.stdout);
|
||||
await p;
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
FROM python:3.10-alpine3.18 as builder
|
||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||
RUN set -x \
|
||||
&& apk update \
|
||||
&& apk add nodejs npm git \
|
||||
&& npm i -g pnpm@8.3.1 pm2 tsx \
|
||||
&& cd /tmp/build \
|
||||
&& pnpm install --prod
|
||||
|
||||
FROM python:3.10-alpine
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||
ARG QL_BRANCH=develop
|
||||
|
||||
ENV PNPM_HOME=/root/.local/share/pnpm \
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
|
||||
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
|
||||
LANG=C.UTF-8 \
|
||||
SHELL=/bin/bash \
|
||||
PS1="\u@\h:\w \$ " \
|
||||
QL_DIR=/ql \
|
||||
QL_BRANCH=${QL_BRANCH}
|
||||
|
||||
VOLUME /ql/data
|
||||
|
||||
EXPOSE 5700
|
||||
|
||||
COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
|
||||
COPY --from=builder /usr/local/bin/. /usr/local/bin/
|
||||
|
||||
RUN set -x \
|
||||
&& apk update -f \
|
||||
&& apk upgrade \
|
||||
&& apk --no-cache add -f bash \
|
||||
coreutils \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
tzdata \
|
||||
perl \
|
||||
openssl \
|
||||
nginx \
|
||||
nodejs \
|
||||
jq \
|
||||
openssh \
|
||||
procps \
|
||||
netcat-openbsd \
|
||||
unzip \
|
||||
npm \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& apk update \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||
&& git config --global user.name "qinglong" \
|
||||
&& git config --global http.postBuffer 524288000 \
|
||||
&& rm -rf /root/.pnpm-store \
|
||||
&& rm -rf /root/.local/share/pnpm/store \
|
||||
&& rm -rf /root/.cache \
|
||||
&& ulimit -c 0
|
||||
|
||||
ARG SOURCE_COMMIT
|
||||
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
&& cd ${QL_DIR} \
|
||||
&& cp -f .env.example .env \
|
||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||
&& mkdir -p ${QL_DIR}/static \
|
||||
&& cp -rf /static/* ${QL_DIR}/static \
|
||||
&& rm -rf /static
|
||||
|
||||
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||
|
||||
WORKDIR ${QL_DIR}
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||
+14
-10
@@ -1,13 +1,13 @@
|
||||
FROM python:3.10-alpine as builder
|
||||
FROM python:3.11-alpine3.18 as builder
|
||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||
RUN set -x \
|
||||
&& apk update \
|
||||
&& apk add nodejs npm git \
|
||||
&& npm i -g pnpm@8.3.1 \
|
||||
&& npm i -g pnpm@8.3.1 pm2 tsx \
|
||||
&& cd /tmp/build \
|
||||
&& pnpm --registry https://registry.npmmirror.com install --prod
|
||||
&& pnpm install --prod
|
||||
|
||||
FROM python:3.10-alpine
|
||||
FROM python:3.11-alpine
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
@@ -23,8 +23,14 @@ ENV PNPM_HOME=/root/.local/share/pnpm \
|
||||
QL_DIR=/ql \
|
||||
QL_BRANCH=${QL_BRANCH}
|
||||
|
||||
VOLUME /ql/data
|
||||
|
||||
EXPOSE 5700
|
||||
|
||||
COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
|
||||
COPY --from=builder /usr/local/bin/. /usr/local/bin/
|
||||
|
||||
RUN set -x \
|
||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update -f \
|
||||
&& apk upgrade \
|
||||
&& apk --no-cache add -f bash \
|
||||
@@ -41,6 +47,7 @@ RUN set -x \
|
||||
openssh \
|
||||
procps \
|
||||
netcat-openbsd \
|
||||
unzip \
|
||||
npm \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& apk update \
|
||||
@@ -49,13 +56,10 @@ RUN set -x \
|
||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||
&& git config --global user.name "qinglong" \
|
||||
&& git config --global http.postBuffer 524288000 \
|
||||
&& npm install -g pnpm@8.3.1 \
|
||||
&& cd && pnpm config set registry https://registry.npmmirror.com \
|
||||
&& pnpm add -g pm2 tsx \
|
||||
&& rm -rf /root/.pnpm-store \
|
||||
&& rm -rf /root/.local/share/pnpm/store \
|
||||
&& rm -rf /root/.cache \
|
||||
&& rm -rf /root/.npm
|
||||
&& ulimit -c 0
|
||||
|
||||
ARG SOURCE_COMMIT
|
||||
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
@@ -73,6 +77,6 @@ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||
WORKDIR ${QL_DIR}
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||
CMD curl -sf http://127.0.0.1:5400/api/health || exit 1
|
||||
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
dir_shell=/ql/shell
|
||||
. $dir_shell/env.sh
|
||||
. $dir_shell/share.sh
|
||||
link_shell
|
||||
|
||||
export isFirstStartServer=true
|
||||
|
||||
echo -e "======================1. 检测配置文件========================\n"
|
||||
make_dir /etc/nginx/conf.d
|
||||
@@ -16,34 +14,24 @@ pm2 l &>/dev/null
|
||||
|
||||
echo -e "======================2. 安装依赖========================\n"
|
||||
patch_version
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
current_npm_registry=$(cd && pnpm config get registry)
|
||||
is_equal_registry=$(echo $current_npm_registry | grep "${NpmMirror}")
|
||||
if [[ "$is_equal_registry" == "" ]]; then
|
||||
cd && pnpm config set registry $NpmMirror
|
||||
pnpm install -g
|
||||
fi
|
||||
update_depend
|
||||
echo
|
||||
|
||||
echo -e "======================3. 启动nginx========================\n"
|
||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||
echo -e "nginx启动成功...\n"
|
||||
|
||||
echo -e "======================4. 启动pm2服务========================\n"
|
||||
reload_update
|
||||
reload_pm2
|
||||
|
||||
if [[ $AutoStartBot == true ]]; then
|
||||
echo -e "======================5. 启动bot========================\n"
|
||||
nohup ql -l bot >$dir_log/bot.log 2>&1 &
|
||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
||||
echo -e "bot后台启动中...\n"
|
||||
fi
|
||||
|
||||
if [[ $EnableExtraShell == true ]]; then
|
||||
echo -e "====================6. 执行自定义脚本========================\n"
|
||||
nohup ql -l extra >$dir_log/extra.log 2>&1 &
|
||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
||||
echo -e "自定义脚本后台执行中...\n"
|
||||
fi
|
||||
|
||||
|
||||
+29
-1
@@ -6,22 +6,42 @@ upstream publicApi {
|
||||
server 0.0.0.0:5400;
|
||||
}
|
||||
|
||||
upstream updateApi {
|
||||
server 0.0.0.0:5300;
|
||||
}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default keep-alive;
|
||||
'websocket' upgrade;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 5700;
|
||||
IPV4_CONFIG
|
||||
IPV6_CONFIG
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
location QL_BASE_URLapi/update/ {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://updateApi/api/;
|
||||
proxy_buffering off;
|
||||
proxy_redirect default;
|
||||
proxy_connect_timeout 1800;
|
||||
proxy_send_timeout 1800;
|
||||
proxy_read_timeout 1800;
|
||||
}
|
||||
|
||||
location QL_BASE_URLapi/public/ {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://publicApi/api/;
|
||||
proxy_buffering off;
|
||||
proxy_redirect default;
|
||||
proxy_connect_timeout 1800;
|
||||
proxy_send_timeout 1800;
|
||||
proxy_read_timeout 1800;
|
||||
}
|
||||
|
||||
location QL_BASE_URLapi/ {
|
||||
@@ -30,6 +50,10 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://baseApi/api/;
|
||||
proxy_buffering off;
|
||||
proxy_redirect default;
|
||||
proxy_connect_timeout 1800;
|
||||
proxy_send_timeout 1800;
|
||||
proxy_read_timeout 1800;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
@@ -41,6 +65,10 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://baseApi/open/;
|
||||
proxy_buffering off;
|
||||
proxy_redirect default;
|
||||
proxy_connect_timeout 1800;
|
||||
proxy_send_timeout 1800;
|
||||
proxy_read_timeout 1800;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ http {
|
||||
|
||||
server_tokens off;
|
||||
|
||||
client_max_body_size 20m;
|
||||
client_max_body_size 4096m;
|
||||
client_body_buffer_size 20m;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
@@ -9,6 +9,14 @@ module.exports = {
|
||||
source_map_support: true,
|
||||
time: true,
|
||||
script: 'static/build/schedule/index.js',
|
||||
env: {
|
||||
http_proxy: '',
|
||||
https_proxy: '',
|
||||
HTTP_PROXY: '',
|
||||
HTTPS_PROXY: '',
|
||||
all_proxy: '',
|
||||
ALL_PROXY: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'public',
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"watch": ["back", ".env"],
|
||||
"ext": "js,ts,json",
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "ts-node --transpile-only ./back/app.ts"
|
||||
"exec": "ts-node -P tsconfig.back.json ./back/app.ts"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'update',
|
||||
max_restarts: 10,
|
||||
kill_timeout: 15000,
|
||||
wait_ready: true,
|
||||
listen_timeout: 10000,
|
||||
time: true,
|
||||
script: 'static/build/update.js',
|
||||
},
|
||||
],
|
||||
};
|
||||
+32
-16
@@ -4,19 +4,24 @@
|
||||
"start": "concurrently -n w: npm:start:*",
|
||||
"start:front": "max dev",
|
||||
"start:back": "nodemon",
|
||||
"start:public": "ts-node --transpile-only ./back/public.ts",
|
||||
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts",
|
||||
"start:update": "ts-node -P tsconfig.back.json ./back/update.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:back": "tsc -p tsconfig.back.json",
|
||||
"panel": "npm run build:back && node static/build/app.js",
|
||||
"schedule": "npm run build:back && node static/build/schedule/index.js",
|
||||
"public": "npm run build:back && node static/build/public.js",
|
||||
"update": "npm run build:back && node static/build/update.js",
|
||||
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"postinstall": "max setup 2>/dev/null || true",
|
||||
"test": "umi-test",
|
||||
"test:coverage": "umi-test --coverage"
|
||||
},
|
||||
"bin": {
|
||||
"task": "./cli/task.mjs"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
@@ -58,7 +63,6 @@
|
||||
"@grpc/grpc-js": "^1.8.13",
|
||||
"@otplib/preset-default": "^12.0.1",
|
||||
"@sentry/node": "^7.12.1",
|
||||
"@sentry/tracing": "^7.12.1",
|
||||
"body-parser": "^1.19.2",
|
||||
"celebrate": "^15.0.1",
|
||||
"chokidar": "^3.5.3",
|
||||
@@ -69,22 +73,22 @@
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"express-jwt": "^6.1.1",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"express-rate-limit": "^7.0.0",
|
||||
"express-urlrewrite": "^1.4.0",
|
||||
"form-data": "^4.0.0",
|
||||
"got": "^11.8.2",
|
||||
"hpagent": "^0.1.2",
|
||||
"hpagent": "^1.2.0",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^1.4.4",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"nedb": "^1.8.0",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"p-limit": "3.1.0",
|
||||
"protobufjs": "^7.2.3",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
"protobufjs": "^7.3.0",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^6.25.5",
|
||||
@@ -95,7 +99,11 @@
|
||||
"typedi": "^0.10.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.6.0",
|
||||
"yargs": "^17.3.1"
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"zx": "^8.1.4",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"request-ip": "3.3.0",
|
||||
"ip2region": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
@@ -108,6 +116,7 @@
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-jwt": "^6.0.4",
|
||||
"@types/file-saver": "2.0.2",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/lodash": "^4.14.185",
|
||||
@@ -124,37 +133,44 @@
|
||||
"@types/sockjs": "^0.3.33",
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@umijs/max": "^4.0.55",
|
||||
"@types/request-ip": "0.0.41",
|
||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||
"@uiw/react-codemirror": "^4.21.9",
|
||||
"@umijs/max": "^4.0.72",
|
||||
"@umijs/ssr-darkreader": "^4.9.45",
|
||||
"ahooks": "^3.7.8",
|
||||
"ansi-to-react": "^6.1.6",
|
||||
"antd": "^4.24.8",
|
||||
"antd-img-crop": "^4.2.3",
|
||||
"codemirror": "^5.65.2",
|
||||
"axios": "^1.4.0",
|
||||
"compression-webpack-plugin": "9.2.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"file-saver": "2.0.2",
|
||||
"lint-staged": "^13.0.3",
|
||||
"monaco-editor": "0.33.0",
|
||||
"nodemon": "^2.0.15",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^2.5.1",
|
||||
"pretty-bytes": "6.1.1",
|
||||
"qiniu": "^7.4.0",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"query-string": "^7.1.1",
|
||||
"rc-tween-one": "^3.0.6",
|
||||
"rc-virtual-list": "3.5.3",
|
||||
"react": "18.2.0",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-diff-viewer": "^3.1.1",
|
||||
"react-dnd": "^14.0.2",
|
||||
"react-dnd-html5-backend": "^14.0.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-intl-universal": "^2.6.21",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.6.0",
|
||||
"ts-node": "^10.6.0",
|
||||
"ts-proto": "^1.146.0",
|
||||
"tslib": "^2.4.0",
|
||||
"tsx": "^3.12.3",
|
||||
"typescript": "4.8.4",
|
||||
"umi-request": "^1.4.0",
|
||||
"tsx": "^4.7.3",
|
||||
"typescript": "5.2.2",
|
||||
"vh-check": "^2.0.5",
|
||||
"virtualizedtableforantd4": "1.3.0",
|
||||
"webpack": "^5.70.0",
|
||||
|
||||
Generated
+1894
-627
File diff suppressed because it is too large
Load Diff
+70
-24
@@ -21,20 +21,19 @@ DiskWarn=90
|
||||
## 设置定时任务执行的超时时间,例如1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||
CommandTimeoutTime=""
|
||||
|
||||
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
|
||||
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
|
||||
RandomDelay="300"
|
||||
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
|
||||
RandomDelay=""
|
||||
|
||||
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
|
||||
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
|
||||
RandomDelayFileExtensions="js"
|
||||
RandomDelayFileExtensions=""
|
||||
|
||||
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
|
||||
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
|
||||
RandomDelayIgnoredMinutes="0 30"
|
||||
RandomDelayIgnoredMinutes=""
|
||||
|
||||
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
|
||||
EnableExtraShell="true"
|
||||
## 如果你自己会写shell脚本,并且希望在每次容器启动时,额外运行你的 shell 脚本,请赋值为 "true"
|
||||
EnableExtraShell=""
|
||||
|
||||
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
|
||||
AutoStartBot=""
|
||||
@@ -42,12 +41,6 @@ AutoStartBot=""
|
||||
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
|
||||
BotRepoUrl=""
|
||||
|
||||
## 安装python依赖时指定pip源
|
||||
PipMirror="https://pypi.doubanio.com/simple/"
|
||||
|
||||
## 安装node依赖时指定npm源
|
||||
NpmMirror="https://registry.npmmirror.com"
|
||||
|
||||
## 通知环境变量
|
||||
## 1. Server酱
|
||||
## https://sct.ftqq.com
|
||||
@@ -63,6 +56,12 @@ export BARK_ICON="https://qn.whyour.cn/logo.png"
|
||||
export BARK_SOUND=""
|
||||
## 下方填写推送消息分组,默认为"QingLong"
|
||||
export BARK_GROUP="QingLong"
|
||||
## bark 推送时效性
|
||||
export BARK_LEVEL="active"
|
||||
## bark 推送是否存档
|
||||
export BARK_ARCHIVE=""
|
||||
## bark 推送跳转 URL
|
||||
export BARK_URL=""
|
||||
|
||||
## 3. Telegram
|
||||
## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw
|
||||
@@ -119,7 +118,18 @@ export PUSH_PLUS_TOKEN=""
|
||||
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
|
||||
export PUSH_PLUS_USER=""
|
||||
|
||||
## 9. go-cqhttp
|
||||
## 9. 微加机器人
|
||||
## 官方网站:http://www.weplusbot.com
|
||||
## 下方填写您的Token;微信扫描登录后在"我的"->"设置"->"令牌"中获取
|
||||
export WE_PLUS_BOT_TOKEN=""
|
||||
## 消息接收人;
|
||||
## 个人版填写接收消息的群编码,不填发送给自己的微信号
|
||||
## 专业版不填默认发给机器人自己,发送给好友填写wxid,发送给微信群填写群编码
|
||||
export WE_PLUS_BOT_RECEIVER=""
|
||||
## 调用版本;分为专业版和个人版,专业版填写pro,个人版填写personal
|
||||
export WE_PLUS_BOT_VERSION="pro"
|
||||
|
||||
## 10. go-cqhttp
|
||||
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||
## gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
||||
@@ -128,7 +138,7 @@ export GOBOT_URL=""
|
||||
export GOBOT_TOKEN=""
|
||||
export GOBOT_QQ=""
|
||||
|
||||
## 10. gotify
|
||||
## 11. gotify
|
||||
## gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||
## gotify_token 填写gotify的消息应用token
|
||||
## gotify_priority 填写推送消息优先级,默认为0
|
||||
@@ -136,17 +146,17 @@ export GOTIFY_URL=""
|
||||
export GOTIFY_TOKEN=""
|
||||
export GOTIFY_PRIORITY=0
|
||||
|
||||
## 11. PushDeer
|
||||
## 12. PushDeer
|
||||
## deer_key 填写PushDeer的key
|
||||
export DEER_KEY=""
|
||||
|
||||
## 12. Chat
|
||||
## 13. Chat
|
||||
## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token=
|
||||
## chat_token 填写后面的token
|
||||
export CHAT_URL=""
|
||||
export CHAT_TOKEN=""
|
||||
|
||||
## 13. aibotk
|
||||
## 14. aibotk
|
||||
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
|
||||
## aibotk_key (必填)填写智能微秘书个人中心的apikey
|
||||
export AIBOTK_KEY=""
|
||||
@@ -155,12 +165,18 @@ export AIBOTK_TYPE=""
|
||||
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
|
||||
export AIBOTK_NAME=""
|
||||
|
||||
## 14. SMTP
|
||||
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
|
||||
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||
export SMTP_SERVER=""
|
||||
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
|
||||
export SMTP_SSL="false"
|
||||
## 15. 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=""
|
||||
|
||||
## 16. SMTP
|
||||
## 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
|
||||
export SMTP_SERVICE=""
|
||||
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
||||
export SMTP_EMAIL=""
|
||||
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||
@@ -168,4 +184,34 @@ export SMTP_PASSWORD=""
|
||||
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
|
||||
export SMTP_NAME=""
|
||||
|
||||
## 17. PushMe
|
||||
## 官方说明文档:https://push.i-i.me/
|
||||
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
|
||||
## PUSHME_URL (选填)填写自建的PushMeServer消息服务接口地址,例如:http://127.0.0.1:3010,不填则使用官方接口服务
|
||||
export PUSHME_KEY=""
|
||||
export PUSHME_URL=""
|
||||
|
||||
## 18. 飞书机器人
|
||||
## 官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
|
||||
## FSKEY 飞书机器人的 FSKEY
|
||||
export FSKEY=""
|
||||
|
||||
## 19. Qmsg酱
|
||||
## 官方文档:https://qmsg.zendee.cn/docs/api/
|
||||
## qmsg 酱的 QMSG_KEY
|
||||
## qmsg 酱的 QMSG_TYPE send 为私聊,group 为群聊
|
||||
export QMSG_KEY=""
|
||||
export QMSG_TYPE=""
|
||||
|
||||
## 20. 自定义通知
|
||||
## 自定义通知 接收回调的URL
|
||||
export WEBHOOK_URL=""
|
||||
## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时,直接换行或者使用 $'\n' 连接多行字符串,比如 export dd="line 1"$'\n'"line 2"
|
||||
export WEBHOOK_BODY=""
|
||||
export WEBHOOK_HEADERS=""
|
||||
## 支持 GET/POST/PUT
|
||||
export WEBHOOK_METHOD=""
|
||||
## 支持 text/plain、application/json、multipart/form-data、application/x-www-form-urlencoded
|
||||
export WEBHOOK_CONTENT_TYPE=""
|
||||
|
||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||
|
||||
+639
-443
File diff suppressed because one or more lines are too long
+317
-64
@@ -33,15 +33,17 @@ def print(text, *args, **kw):
|
||||
# 通知服务
|
||||
# fmt: off
|
||||
push_config = {
|
||||
'HITOKOTO': False, # 启用一言(随机句子)
|
||||
'HITOKOTO': True, # 启用一言(随机句子)
|
||||
|
||||
'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
|
||||
'BARK_ARCHIVE': '', # bark 推送是否存档
|
||||
'BARK_GROUP': '', # bark 推送分组
|
||||
'BARK_SOUND': '', # bark 推送声音
|
||||
'BARK_ICON': '', # bark 推送图标
|
||||
'BARK_LEVEL': '', # bark 推送时效性
|
||||
'BARK_URL': '', # bark 推送跳转URL
|
||||
|
||||
'CONSOLE': True, # 控制台输出
|
||||
'CONSOLE': False, # 控制台输出
|
||||
|
||||
'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
|
||||
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
|
||||
@@ -73,6 +75,10 @@ push_config = {
|
||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||
|
||||
'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
|
||||
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
|
||||
'WE_PLUS_BOT_VERSION': 'pro', # 微加机器人的调用版本
|
||||
|
||||
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
|
||||
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
||||
|
||||
@@ -98,11 +104,22 @@ push_config = {
|
||||
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
|
||||
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
||||
|
||||
'PUSHME_KEY': '', # PushMe 的 PUSHME_KEY
|
||||
'PUSHME_URL': '', # PushMe 的 PUSHME_URL
|
||||
|
||||
'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 = []
|
||||
# fmt: on
|
||||
|
||||
# 首先读取 面板变量 或者 github action 运行变量
|
||||
for k in push_config:
|
||||
if os.getenv(k):
|
||||
v = os.getenv(k)
|
||||
@@ -119,17 +136,22 @@ def bark(title: str, content: str) -> None:
|
||||
print("bark 服务启动")
|
||||
|
||||
if push_config.get("BARK_PUSH").startswith("http"):
|
||||
url = f'{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
|
||||
url = f'{push_config.get("BARK_PUSH")}'
|
||||
else:
|
||||
url = f'https://api.day.app/{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
|
||||
url = f'https://api.day.app/{push_config.get("BARK_PUSH")}'
|
||||
|
||||
bark_params = {
|
||||
"BARK_ARCHIVE": "isArchive",
|
||||
"BARK_GROUP": "group",
|
||||
"BARK_SOUND": "sound",
|
||||
"BARK_ICON": "icon",
|
||||
"BARK_LEVEL": "level",
|
||||
"BARK_URL": "url",
|
||||
}
|
||||
data = {
|
||||
"title": title,
|
||||
"body": content,
|
||||
}
|
||||
params = ""
|
||||
for pair in filter(
|
||||
lambda pairs: pairs[0].startswith("BARK_")
|
||||
and pairs[0] != "BARK_PUSH"
|
||||
@@ -137,10 +159,11 @@ def bark(title: str, content: str) -> None:
|
||||
and bark_params.get(pairs[0]),
|
||||
push_config.items(),
|
||||
):
|
||||
params += f"{bark_params.get(pair[0])}={pair[1]}&"
|
||||
if params:
|
||||
url = url + "?" + params.rstrip("&")
|
||||
response = requests.get(url).json()
|
||||
data[bark_params.get(pair[0])] = pair[1]
|
||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||
response = requests.post(
|
||||
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||
).json()
|
||||
|
||||
if response["code"] == 200:
|
||||
print("bark 推送成功!")
|
||||
@@ -198,7 +221,7 @@ def feishu_bot(title: str, content: str) -> None:
|
||||
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
|
||||
response = requests.post(url, data=json.dumps(data)).json()
|
||||
|
||||
if response.get("StatusCode") == 0:
|
||||
if response.get("StatusCode") == 0 or response.get("code") == 0:
|
||||
print("飞书 推送成功!")
|
||||
else:
|
||||
print("飞书 推送失败!错误信息如下:\n", response)
|
||||
@@ -366,6 +389,38 @@ def pushplus_bot(title: str, content: str) -> None:
|
||||
print("PUSHPLUS 推送失败!")
|
||||
|
||||
|
||||
def weplus_bot(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 微加机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("WE_PLUS_BOT_TOKEN"):
|
||||
print("微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("微加机器人 服务启动")
|
||||
|
||||
template = "txt"
|
||||
if len(content) > 800:
|
||||
template = "html"
|
||||
|
||||
url = "https://www.weplusbot.com/send"
|
||||
data = {
|
||||
"token": push_config.get("WE_PLUS_BOT_TOKEN"),
|
||||
"title": title,
|
||||
"content": content,
|
||||
"template": template,
|
||||
"receiver": push_config.get("WE_PLUS_BOT_RECEIVER"),
|
||||
"version": push_config.get("WE_PLUS_BOT_VERSION"),
|
||||
}
|
||||
body = json.dumps(data).encode(encoding="utf-8")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(url=url, data=body, headers=headers).json()
|
||||
|
||||
if response["code"] == 200:
|
||||
print("微加机器人 推送成功!")
|
||||
else:
|
||||
print("微加机器人 推送失败!")
|
||||
|
||||
|
||||
def qmsg_bot(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 qmsg 推送消息。
|
||||
@@ -440,7 +495,9 @@ class WeCom:
|
||||
return data["access_token"]
|
||||
|
||||
def send_text(self, message, touser="@all"):
|
||||
send_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||
send_url = (
|
||||
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||
)
|
||||
send_values = {
|
||||
"touser": touser,
|
||||
"msgtype": "text",
|
||||
@@ -454,7 +511,9 @@ class WeCom:
|
||||
return respone["errmsg"]
|
||||
|
||||
def send_mpnews(self, title, message, media_id, touser="@all"):
|
||||
send_url = f"https://{self.HOST}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||
send_url = (
|
||||
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||
)
|
||||
send_values = {
|
||||
"touser": touser,
|
||||
"msgtype": "mpnews",
|
||||
@@ -514,7 +573,7 @@ def telegram_bot(title: str, content: str) -> None:
|
||||
print("tg 服务启动")
|
||||
|
||||
if push_config.get("TG_API_HOST"):
|
||||
url = f"https://{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||
url = f"{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||
else:
|
||||
url = (
|
||||
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||
@@ -558,7 +617,9 @@ def aibotk(title: str, content: str) -> None:
|
||||
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
|
||||
print("智能微秘书 服务启动")
|
||||
|
||||
@@ -638,6 +699,174 @@ def smtp(title: str, content: str) -> None:
|
||||
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 = (
|
||||
push_config.get("PUSHME_URL")
|
||||
if push_config.get("PUSHME_URL")
|
||||
else "https://push.i-i.me/"
|
||||
)
|
||||
data = {
|
||||
"push_key": push_config.get("PUSHME_KEY"),
|
||||
"title": title,
|
||||
"content": content,
|
||||
"date": push_config.get("date") if push_config.get("date") else "",
|
||||
"type": push_config.get("type") if push_config.get("type") else "",
|
||||
}
|
||||
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_string(input_string, value_format_fn=None):
|
||||
matches = {}
|
||||
pattern = r"(\w+):\s*((?:(?!\n\w+:).)*)"
|
||||
regex = re.compile(pattern)
|
||||
for match in regex.finditer(input_string):
|
||||
key, value = match.group(1).strip(), match.group(2).strip()
|
||||
try:
|
||||
value = value_format_fn(value) if value_format_fn else value
|
||||
json_value = json.loads(value)
|
||||
matches[key] = json_value
|
||||
except:
|
||||
matches[key] = value
|
||||
return matches
|
||||
|
||||
|
||||
def parse_body(body, content_type, value_format_fn=None):
|
||||
if not body or content_type == "text/plain":
|
||||
return value_format_fn(body) if value_format_fn and body else body
|
||||
|
||||
parsed = parse_string(body, value_format_fn)
|
||||
|
||||
if content_type == "application/x-www-form-urlencoded":
|
||||
data = urllib.parse.urlencode(parsed, doseq=True)
|
||||
return data
|
||||
|
||||
if content_type == "application/json":
|
||||
data = json.dumps(parsed)
|
||||
return data
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
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")
|
||||
|
||||
if "$title" not in WEBHOOK_URL and "$title" not in WEBHOOK_BODY:
|
||||
print("请求头或者请求体中必须包含 $title 和 $content")
|
||||
return
|
||||
|
||||
headers = parse_headers(WEBHOOK_HEADERS)
|
||||
body = parse_body(
|
||||
WEBHOOK_BODY,
|
||||
WEBHOOK_CONTENT_TYPE,
|
||||
lambda v: v.replace("$title", title).replace("$content", content),
|
||||
)
|
||||
formatted_url = WEBHOOK_URL.replace(
|
||||
"$title", urllib.parse.quote_plus(title)
|
||||
).replace("$content", urllib.parse.quote_plus(content))
|
||||
response = requests.request(
|
||||
method=WEBHOOK_METHOD, url=formatted_url, headers=headers, timeout=15, data=body
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("自定义通知推送成功!")
|
||||
else:
|
||||
print(f"自定义通知推送失败!{response.status_code} {response.text}")
|
||||
|
||||
|
||||
def one() -> str:
|
||||
"""
|
||||
获取一条一言。
|
||||
@@ -648,53 +877,78 @@ def one() -> str:
|
||||
return res["hitokoto"] + " ----" + res["from"]
|
||||
|
||||
|
||||
if push_config.get("BARK_PUSH"):
|
||||
notify_function.append(bark)
|
||||
if push_config.get("CONSOLE"):
|
||||
notify_function.append(console)
|
||||
if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
|
||||
notify_function.append(dingding_bot)
|
||||
if push_config.get("FSKEY"):
|
||||
notify_function.append(feishu_bot)
|
||||
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
|
||||
notify_function.append(go_cqhttp)
|
||||
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
|
||||
notify_function.append(gotify)
|
||||
if push_config.get("IGOT_PUSH_KEY"):
|
||||
notify_function.append(iGot)
|
||||
if push_config.get("PUSH_KEY"):
|
||||
notify_function.append(serverJ)
|
||||
if push_config.get("DEER_KEY"):
|
||||
notify_function.append(pushdeer)
|
||||
if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"):
|
||||
notify_function.append(chat)
|
||||
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||
notify_function.append(pushplus_bot)
|
||||
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||
notify_function.append(qmsg_bot)
|
||||
if push_config.get("QYWX_AM"):
|
||||
notify_function.append(wecom_app)
|
||||
if push_config.get("QYWX_KEY"):
|
||||
notify_function.append(wecom_bot)
|
||||
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
|
||||
notify_function.append(telegram_bot)
|
||||
if (
|
||||
push_config.get("AIBOTK_KEY")
|
||||
and push_config.get("AIBOTK_TYPE")
|
||||
and push_config.get("AIBOTK_NAME")
|
||||
):
|
||||
notify_function.append(aibotk)
|
||||
if (
|
||||
push_config.get("SMTP_SERVER")
|
||||
and push_config.get("SMTP_SSL")
|
||||
and push_config.get("SMTP_EMAIL")
|
||||
and push_config.get("SMTP_PASSWORD")
|
||||
and push_config.get("SMTP_NAME")
|
||||
):
|
||||
notify_function.append(smtp)
|
||||
def add_notify_function():
|
||||
notify_function = []
|
||||
if push_config.get("BARK_PUSH"):
|
||||
notify_function.append(bark)
|
||||
if push_config.get("CONSOLE"):
|
||||
notify_function.append(console)
|
||||
if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
|
||||
notify_function.append(dingding_bot)
|
||||
if push_config.get("FSKEY"):
|
||||
notify_function.append(feishu_bot)
|
||||
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
|
||||
notify_function.append(go_cqhttp)
|
||||
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
|
||||
notify_function.append(gotify)
|
||||
if push_config.get("IGOT_PUSH_KEY"):
|
||||
notify_function.append(iGot)
|
||||
if push_config.get("PUSH_KEY"):
|
||||
notify_function.append(serverJ)
|
||||
if push_config.get("DEER_KEY"):
|
||||
notify_function.append(pushdeer)
|
||||
if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"):
|
||||
notify_function.append(chat)
|
||||
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||
notify_function.append(pushplus_bot)
|
||||
if push_config.get("WE_PLUS_BOT_TOKEN"):
|
||||
notify_function.append(weplus_bot)
|
||||
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||
notify_function.append(qmsg_bot)
|
||||
if push_config.get("QYWX_AM"):
|
||||
notify_function.append(wecom_app)
|
||||
if push_config.get("QYWX_KEY"):
|
||||
notify_function.append(wecom_bot)
|
||||
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
|
||||
notify_function.append(telegram_bot)
|
||||
if (
|
||||
push_config.get("AIBOTK_KEY")
|
||||
and push_config.get("AIBOTK_TYPE")
|
||||
and push_config.get("AIBOTK_NAME")
|
||||
):
|
||||
notify_function.append(aibotk)
|
||||
if (
|
||||
push_config.get("SMTP_SERVER")
|
||||
and push_config.get("SMTP_SSL")
|
||||
and push_config.get("SMTP_EMAIL")
|
||||
and push_config.get("SMTP_PASSWORD")
|
||||
and push_config.get("SMTP_NAME")
|
||||
):
|
||||
notify_function.append(smtp)
|
||||
if push_config.get("PUSHME_KEY"):
|
||||
notify_function.append(pushme)
|
||||
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)
|
||||
|
||||
if not notify_function:
|
||||
print(f"无推送渠道,请检查通知变量是否正确")
|
||||
return notify_function
|
||||
|
||||
|
||||
def send(title: str, content: str) -> None:
|
||||
def send(title: str, content: str, ignore_default_config: bool = False, **kwargs):
|
||||
if kwargs:
|
||||
global push_config
|
||||
if ignore_default_config:
|
||||
push_config = kwargs # 清空从环境变量获取的配置
|
||||
else:
|
||||
push_config.update(kwargs)
|
||||
|
||||
if not content:
|
||||
print(f"{title} 推送内容为空!")
|
||||
return
|
||||
@@ -707,10 +961,9 @@ def send(title: str, content: str) -> None:
|
||||
return
|
||||
|
||||
hitokoto = push_config.get("HITOKOTO")
|
||||
content += "\n\n" + one() if hitokoto != "false" else ""
|
||||
|
||||
text = one() if hitokoto else ""
|
||||
content += "\n\n" + text
|
||||
|
||||
notify_function = add_notify_function()
|
||||
ts = [
|
||||
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
|
||||
for mode in notify_function
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "dependence",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"got": "^11.5.1",
|
||||
"nodemailer": "^6.8.0",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"tunnel": "0.0.6",
|
||||
"ws": "^7.4.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 任务名称
|
||||
* name: script name
|
||||
* 定时规则
|
||||
* cron: 1 9 * * *
|
||||
*/
|
||||
const { sendNotify } = require('./sendNotify.js'); // commonjs
|
||||
// import { sendNotify } from './sendNotify'; // es6
|
||||
|
||||
console.log('test scripts');
|
||||
sendNotify('test scripts', 'test desc');
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
任务名称
|
||||
name: script name
|
||||
定时规则
|
||||
cron: 1 9 * * *
|
||||
"""
|
||||
import notify
|
||||
|
||||
print("test script")
|
||||
notify.send('test script', 'test desc')
|
||||
+5
-5
@@ -36,7 +36,7 @@ add_cron_api() {
|
||||
local sub_id="$4"
|
||||
fi
|
||||
|
||||
if [[ ! $sub_id ]];then
|
||||
if [[ ! $sub_id ]]; then
|
||||
sub_id="null"
|
||||
fi
|
||||
|
||||
@@ -49,7 +49,7 @@ add_cron_api() {
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
|
||||
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
@@ -85,7 +85,7 @@ update_cron_api() {
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
|
||||
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
@@ -117,7 +117,7 @@ update_cron_command_api() {
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
|
||||
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
@@ -196,7 +196,7 @@ notify_api() {
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
|
||||
--data-raw "{\"title\":\"${title//\"/\\\"}\",\"content\":\"${content//\"/\\\"}\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
|
||||
@@ -25,9 +25,6 @@ fi
|
||||
echo -e "\nbot文件下载成功...\n"
|
||||
|
||||
echo -e "3、安装python3依赖...\n"
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
|
||||
|
||||
cd $dir_data
|
||||
|
||||
+2
-15
@@ -9,9 +9,6 @@ reset_env() {
|
||||
npm_install_2 $dir_root
|
||||
echo -e "---> 青龙依赖安装完成\n"
|
||||
|
||||
echo -e "---> 3. 开始安装脚本依赖\n"
|
||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||
npm_install_2 $dir_scripts
|
||||
echo -e "---> 脚本依赖安装完成\n"
|
||||
}
|
||||
|
||||
@@ -76,25 +73,15 @@ check_pm2() {
|
||||
|
||||
main() {
|
||||
echo -e "=====> 开始检测"
|
||||
npm i -g pnpm@8.3.1
|
||||
npm i -g pnpm@8.3.1 pm2 tsx
|
||||
patch_version
|
||||
|
||||
apk add procps netcat-openbsd
|
||||
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
if [[ $NpmMirror ]]; then
|
||||
cd && pnpm config set registry $NpmMirror
|
||||
pnpm install -g
|
||||
fi
|
||||
|
||||
pnpm add -g pm2 tsx
|
||||
reset_env
|
||||
copy_dep
|
||||
check_ql
|
||||
check_nginx
|
||||
check_pm2
|
||||
reload_update
|
||||
reload_pm2
|
||||
echo -e "\n=====> 检测结束\n"
|
||||
}
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
store_env_vars() {
|
||||
initial_vars=($(env | cut -d= -f1))
|
||||
}
|
||||
|
||||
restore_env_vars() {
|
||||
for key in $(env | cut -d= -f1); do
|
||||
if ! [[ " ${initial_vars[@]} " =~ " $key " ]]; then
|
||||
unset "$key"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
store_env_vars
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user