mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,51 @@ name: Build And Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
branches:
|
||||
- 'master'
|
||||
- 'develop'
|
||||
- "master"
|
||||
- "develop"
|
||||
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 +78,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 +122,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 +142,80 @@ 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 }}
|
||||
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
|
||||
# platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=whyour/qinglong:cache
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
|
||||
|
||||
- name: Image digest
|
||||
run: |
|
||||
echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
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 }}
|
||||
@@ -158,12 +224,12 @@ jobs:
|
||||
network: host
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
file: ./docker/310.Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=whyour/qinglong:cache
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
|
||||
|
||||
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.outputs.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',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
+96
-57
@@ -35,6 +35,81 @@ 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/centos/alpine` systems and requires `node/python3` to be installed.
|
||||
|
||||
```bash
|
||||
npm i @whyour/qinglong
|
||||
```
|
||||
|
||||
## 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
|
||||
ql update
|
||||
# Run custom scripts extra.sh
|
||||
ql extra
|
||||
# Adding a single script file
|
||||
ql raw <file_url>
|
||||
# Add a specific script for a single repository
|
||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
||||
# Delete old logs
|
||||
ql rmlog <days>
|
||||
# Start bot
|
||||
ql bot
|
||||
# Detecting the Green Dragon environment and repairing it
|
||||
ql check
|
||||
# Reset the number of login errors
|
||||
ql resetlet
|
||||
# Disable two-step login
|
||||
ql resettfa
|
||||
```
|
||||
|
||||
| **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 |
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker (Recommended)
|
||||
@@ -43,7 +118,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,7 +150,12 @@ 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
|
||||
@@ -81,6 +166,14 @@ podman run -dit \
|
||||
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3 yourself
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
# Centos
|
||||
curl --silent --location https://rpm.nodesource.com/setup_20.x | sudo 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,64 +183,10 @@ export QL_DATA_DIR=""
|
||||
qinglong
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
1. built-in commands
|
||||
|
||||
```bash
|
||||
# Update and restart Green Dragon
|
||||
ql update
|
||||
# Run custom scripts extra.sh
|
||||
ql extra
|
||||
# Adding a single script file
|
||||
ql raw <file_url>
|
||||
# Add a specific script for a single repository
|
||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
||||
# Delete old logs
|
||||
ql rmlog <days>
|
||||
# Start bot
|
||||
ql bot
|
||||
# Detecting the Green Dragon environment and repairing it
|
||||
ql check
|
||||
# Reset the number of login errors
|
||||
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
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
$ git clone git@github.com:whyour/qinglong.git
|
||||
$ git clone https://github.com/whyour/qinglong.git
|
||||
$ cd qinglong
|
||||
$ cp .env.example .env
|
||||
# Recommended use pnpm https://pnpm.io/zh/installation
|
||||
@@ -156,7 +195,7 @@ $ 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
|
||||
|
||||
@@ -174,4 +213,4 @@ The Green Dragon, also known as the Canglong, is one of the four elephants and o
|
||||
|
||||
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
|
||||
|
||||
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
||||
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
||||
|
||||
@@ -37,6 +37,79 @@ 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/centos/alpine` 系统,需要自行安装 `node/python3`
|
||||
|
||||
```bash
|
||||
npm i @whyour/qinglong
|
||||
```
|
||||
|
||||
## 内置命令
|
||||
|
||||
- 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
|
||||
# 更新并重启青龙
|
||||
ql update
|
||||
# 运行自定义脚本extra.sh
|
||||
ql extra
|
||||
# 添加单个脚本文件
|
||||
ql raw <file_url>
|
||||
# 添加单个仓库的指定脚本
|
||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
||||
# 删除旧日志
|
||||
ql rmlog <days>
|
||||
# 启动tg-bot
|
||||
ql bot
|
||||
# 检测青龙环境并修复
|
||||
ql check
|
||||
# 重置登录错误次数
|
||||
ql resetlet
|
||||
# 禁用两步登录
|
||||
ql resettfa
|
||||
```
|
||||
|
||||
| **参数** | **说明** |
|
||||
|------------|---------------------------------------------------------------------------------------------|
|
||||
| file_url | 脚本地址 |
|
||||
| repo_url | 仓库地址 |
|
||||
| whitelist | 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割 |
|
||||
| blacklist | 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割 |
|
||||
| dependence | 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割 |
|
||||
| extensions | 拉取仓库的文件后缀,多个竖线分割 |
|
||||
| branch | 拉取仓库的分支 |
|
||||
| days | 需要保留的日志的天数 |
|
||||
| file_path | 任务执行时的文件路径 |
|
||||
|
||||
## 部署
|
||||
|
||||
### docker (推荐)
|
||||
@@ -45,9 +118,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,9 +150,12 @@ 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
|
||||
@@ -87,6 +166,14 @@ podman run -dit \
|
||||
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
# Centos
|
||||
curl --silent --location https://rpm.nodesource.com/setup_20.x | sudo bash
|
||||
```
|
||||
|
||||
```bash
|
||||
npm install -g node-pre-gyp pnpm@8.3.1
|
||||
npm install -g @whyour/qinglong
|
||||
qinglong
|
||||
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
|
||||
@@ -96,63 +183,10 @@ export QL_DATA_DIR=""
|
||||
qinglong
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
1. 内置命令
|
||||
|
||||
```bash
|
||||
# 更新并重启青龙
|
||||
ql update
|
||||
# 运行自定义脚本extra.sh
|
||||
ql extra
|
||||
# 添加单个脚本文件
|
||||
ql raw <file_url>
|
||||
# 添加单个仓库的指定脚本
|
||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
||||
# 删除旧日志
|
||||
ql rmlog <days>
|
||||
# 启动tg-bot
|
||||
ql bot
|
||||
# 检测青龙环境并修复
|
||||
ql check
|
||||
# 重置登录错误次数
|
||||
ql resetlet
|
||||
# 禁用两步登录
|
||||
ql resettfa
|
||||
|
||||
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||
task <file_path>
|
||||
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||
task <file_path> 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"代表天
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
$ git clone git@github.com:whyour/qinglong.git
|
||||
$ git clone https://github.com/whyour/qinglong.git
|
||||
$ cd qinglong
|
||||
$ cp .env.example .env
|
||||
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
||||
@@ -161,7 +195,7 @@ $ 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -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({
|
||||
@@ -200,7 +201,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];
|
||||
|
||||
+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, req.file.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: '更新成功' });
|
||||
|
||||
+2
-1
@@ -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();
|
||||
@@ -18,10 +17,12 @@ async function startServer() {
|
||||
const server = app
|
||||
.listen(config.port, () => {
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
+15
-1
@@ -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';
|
||||
|
||||
@@ -20,6 +20,8 @@ const rootPath = process.env.QL_DIR as string;
|
||||
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
const shellPath = path.join(rootPath, 'shell/');
|
||||
const tmpPath = path.join(rootPath, '.tmp/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
@@ -28,6 +30,7 @@ const logPath = path.join(dataPath, 'log/');
|
||||
const dbPath = path.join(dataPath, 'db/');
|
||||
const uploadPath = path.join(dataPath, 'upload/');
|
||||
const sshdPath = path.join(dataPath, 'ssh.d/');
|
||||
const systemLogPath = path.join(dataPath, 'syslog/');
|
||||
|
||||
const envFile = path.join(configPath, 'env.sh');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
@@ -42,6 +45,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 +57,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 +66,11 @@ export default {
|
||||
prefix: '/api',
|
||||
},
|
||||
rootPath,
|
||||
tmpPath,
|
||||
dataPath,
|
||||
dataTgzFile,
|
||||
shareShellFile,
|
||||
dependenceProxyFile,
|
||||
configString,
|
||||
loginFaild,
|
||||
authError,
|
||||
@@ -80,6 +92,7 @@ export default {
|
||||
'config.sh.sample',
|
||||
'cookie.sh',
|
||||
'crontab.list',
|
||||
'dependence-proxy.sh',
|
||||
'env.sh',
|
||||
'token.json',
|
||||
],
|
||||
@@ -102,4 +115,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('');
|
||||
}
|
||||
@@ -34,9 +34,11 @@ export function formatCommand(doc: Subscription, url?: string) {
|
||||
if (type === 'file') {
|
||||
command += `raw "${_url}"`;
|
||||
} else {
|
||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${dependences || ''
|
||||
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
||||
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
||||
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.stat(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.stat(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.stat(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 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,
|
||||
|
||||
+18
-1
@@ -15,8 +15,10 @@ export enum NotificationMode {
|
||||
'iGot' = 'iGot',
|
||||
'pushPlus' = 'pushPlus',
|
||||
'email' = 'email',
|
||||
'pushMe' = 'pushMe',
|
||||
'feishu' = 'feishu',
|
||||
'webhook' = 'webhook',
|
||||
'chronocat' = 'Chronocat',
|
||||
}
|
||||
|
||||
abstract class NotificationBaseInfo {
|
||||
@@ -54,6 +56,8 @@ export class BarkNotification extends NotificationBaseInfo {
|
||||
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
||||
public barkSound = '';
|
||||
public barkGroup = 'qinglong';
|
||||
public barkLevel = 'active';
|
||||
public barkUrl = '';
|
||||
}
|
||||
|
||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||
@@ -62,7 +66,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 {
|
||||
@@ -101,6 +105,16 @@ export class EmailNotification extends NotificationBaseInfo {
|
||||
public emailPass: string = '';
|
||||
}
|
||||
|
||||
export class PushMeNotification extends NotificationBaseInfo {
|
||||
public pushMeKey: 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 = '';
|
||||
@@ -131,5 +145,8 @@ export interface NotificationInfo
|
||||
IGotNotification,
|
||||
PushPlusNotification,
|
||||
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>(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { NotificationInfo } from './notify';
|
||||
export class AuthInfo {
|
||||
ip?: string;
|
||||
type: AuthDataType;
|
||||
info?: AuthModelInfo;
|
||||
info?: SystemModelInfo;
|
||||
id?: number;
|
||||
|
||||
constructor(options: AuthInfo) {
|
||||
@@ -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<AuthInfo, AuthInfo>, AuthInfo { }
|
||||
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';
|
||||
|
||||
+24
-14
@@ -7,18 +7,28 @@ import linkDeps from './deps';
|
||||
import initTask from './initTask';
|
||||
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
await depInjectorLoader();
|
||||
Logger.info('✌️ Dependency Injector loaded');
|
||||
|
||||
await expressLoader({ app: expressApp });
|
||||
Logger.info('✌️ Express loaded');
|
||||
|
||||
await initData();
|
||||
Logger.info('✌️ init data loaded');
|
||||
|
||||
await linkDeps();
|
||||
Logger.info('✌️ link deps loaded');
|
||||
|
||||
initTask();
|
||||
Logger.info('✌️ init task loaded');
|
||||
try {
|
||||
depInjectorLoader();
|
||||
Logger.info('✌️ Dependency Injector loaded');
|
||||
console.log('✌️ Dependency Injector loaded');
|
||||
|
||||
expressLoader({ app: expressApp });
|
||||
Logger.info('✌️ Express loaded');
|
||||
console.log('✌️ Express loaded');
|
||||
|
||||
await initData();
|
||||
Logger.info('✌️ init data loaded');
|
||||
console.log('✌️ init data loaded');
|
||||
|
||||
await linkDeps();
|
||||
Logger.info('✌️ link deps loaded');
|
||||
console.log('✌️ link deps loaded');
|
||||
|
||||
initTask();
|
||||
Logger.info('✌️ init task loaded');
|
||||
console.log('✌️ init task loaded');
|
||||
} catch (error) {
|
||||
Logger.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed, ${error}`);
|
||||
console.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
+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);
|
||||
}
|
||||
};
|
||||
|
||||
+11
-14
@@ -1,20 +1,19 @@
|
||||
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() {
|
||||
@@ -34,16 +33,14 @@ async function linkCommand() {
|
||||
for (const link of linkShell) {
|
||||
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) => {});
|
||||
await rmPath(target);
|
||||
await fs.symlink(source, 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 (
|
||||
|
||||
+51
-17
@@ -9,11 +9,58 @@ 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);
|
||||
|
||||
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 +77,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({
|
||||
@@ -127,4 +158,7 @@ export default async () => {
|
||||
// 初始化保存一次ck和定时任务数据
|
||||
await cronService.autosave_crontab();
|
||||
await envService.set_envs();
|
||||
|
||||
// 初始化增加系统配置
|
||||
await SystemModel.upsert({ type: AuthDataType.systemConfig });
|
||||
};
|
||||
|
||||
+29
-19
@@ -1,11 +1,10 @@
|
||||
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 rootPath = process.env.QL_DIR as string;
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
@@ -28,6 +27,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 +39,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 +47,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;
|
||||
|
||||
+5
-2
@@ -6,14 +6,15 @@ 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 });
|
||||
});
|
||||
@@ -25,9 +26,11 @@ 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 });
|
||||
}
|
||||
}
|
||||
+118
-85
@@ -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,16 @@ export default class CronService {
|
||||
options.last_running_time = last_running_time;
|
||||
}
|
||||
|
||||
return await CrontabModel.update({ ...options }, { where: { id: ids } });
|
||||
for (const id of ids) {
|
||||
const cron = await this.getDb({ id });
|
||||
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
|
||||
options = omit(options, ['status', 'log_path', 'pid']);
|
||||
}
|
||||
await CrontabModel.update(
|
||||
{ ...pickBy(options, (v) => v === 0 || !!v) },
|
||||
{ where: { id } },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
@@ -165,7 +186,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 +197,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 +333,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 = [
|
||||
@@ -374,7 +395,7 @@ export default class CronService {
|
||||
try {
|
||||
await killTask(doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.silly(error);
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,65 +406,57 @@ export default class CronService {
|
||||
);
|
||||
}
|
||||
|
||||
private async runSingle(cronId: number): Promise<number> {
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
private async runSingle(cronId: number): Promise<number | void> {
|
||||
return taskLimit.runWithCronLimit(() => {
|
||||
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}`;
|
||||
}
|
||||
if (
|
||||
cmdStr.endsWith('.js') ||
|
||||
cmdStr.endsWith('.py') ||
|
||||
cmdStr.endsWith('.pyc') ||
|
||||
cmdStr.endsWith('.sh') ||
|
||||
cmdStr.endsWith('.ts')
|
||||
) {
|
||||
cmdStr = `${cmdStr} now`;
|
||||
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 });
|
||||
}
|
||||
const logPath = `${uniqPath}/${logTime}.log`;
|
||||
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
||||
|
||||
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
|
||||
const cp = spawn(
|
||||
`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron)}`,
|
||||
{ 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.stderr.on('data', async (data) => {
|
||||
await fs.appendFile(`${absolutePath}`, `${data.toString()}`);
|
||||
});
|
||||
cp.on('error', (err) => {
|
||||
if (logFileExist) {
|
||||
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||
}
|
||||
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 +472,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 +493,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 +507,41 @@ 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.stat(`${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) {
|
||||
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 = `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 +550,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 +613,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,7 +59,9 @@ 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);
|
||||
}
|
||||
|
||||
+166
-29
@@ -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 },
|
||||
@@ -137,9 +187,17 @@ export default class DependenceService {
|
||||
}
|
||||
|
||||
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 +205,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 +239,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 +314,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 +323,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 +332,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 +348,7 @@ export default class DependenceService {
|
||||
message,
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, message);
|
||||
this.updateLog(depIds, message);
|
||||
|
||||
let status = null;
|
||||
if (isSucceed) {
|
||||
@@ -233,7 +360,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) {
|
||||
|
||||
@@ -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,
|
||||
@@ -202,14 +202,12 @@ 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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(config.envFile, env_string);
|
||||
await fs.writeFile(config.envFile, env_string);
|
||||
}
|
||||
}
|
||||
|
||||
+109
-35
@@ -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 {
|
||||
@@ -28,8 +28,10 @@ export default class NotificationService {
|
||||
['iGot', this.iGot],
|
||||
['pushPlus', this.pushPlus],
|
||||
['email', this.email],
|
||||
['pushMe', this.pushMe],
|
||||
['webhook', this.webhook],
|
||||
['lark', this.lark],
|
||||
['chronocat', this.chronocat],
|
||||
]);
|
||||
|
||||
private title = '';
|
||||
@@ -55,7 +57,7 @@ export default class NotificationService {
|
||||
try {
|
||||
return await notificationModeAction?.call(this);
|
||||
} catch (error: any) {
|
||||
return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -194,7 +196,8 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async bark() {
|
||||
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
||||
let { barkPush, barkIcon, barkSound, barkGroup, barkLevel, barkUrl } =
|
||||
this.params;
|
||||
if (!barkPush.startsWith('http')) {
|
||||
barkPush = `https://api.day.app/${barkPush}`;
|
||||
}
|
||||
@@ -202,7 +205,7 @@ export default class NotificationService {
|
||||
this.title,
|
||||
)}/${encodeURIComponent(
|
||||
this.content,
|
||||
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
|
||||
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}&level=${barkLevel}&url=${barkUrl}`;
|
||||
|
||||
try {
|
||||
const res: any = await got
|
||||
@@ -231,8 +234,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 +306,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 +332,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`;
|
||||
@@ -556,6 +561,87 @@ export default class NotificationService {
|
||||
} else {
|
||||
throw new Error(JSON.stringify(info));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async pushMe() {
|
||||
const { pushMeKey } = this.params;
|
||||
try {
|
||||
const res: any = await got.post(
|
||||
`https://push.i-i.me/?push_key=${pushMeKey}`,
|
||||
{
|
||||
...this.gotOption,
|
||||
json: {
|
||||
title: this.title,
|
||||
content: this.content,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
);
|
||||
if (res.body === 'success') {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(res.body);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async chronocat() {
|
||||
const { chronocatURL, chronocatQQ, 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 +656,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 +673,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 +695,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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+39
-28
@@ -12,7 +12,7 @@ import dayjs from 'dayjs';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import { spawn } from 'cross-spawn';
|
||||
|
||||
interface ScheduleTaskType {
|
||||
export interface ScheduleTaskType {
|
||||
id: number;
|
||||
command: string;
|
||||
name?: 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);
|
||||
}
|
||||
|
||||
|
||||
+12
-9
@@ -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,
|
||||
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,10 @@ export default class ScriptService {
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async getFile(filePath: string, fileName: string) {
|
||||
const _filePath = join(config.scriptPath, 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[]> {
|
||||
@@ -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,13 +256,24 @@ export default class SubscriptionService {
|
||||
);
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
public async remove(ids: number[], query: any) {
|
||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
await this.handleTask(doc, false);
|
||||
}
|
||||
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(
|
||||
@@ -286,17 +300,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 +319,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 +351,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 +363,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.stat(`${dir}/${x}`)).mtime.getTime(),
|
||||
})),
|
||||
)
|
||||
).sort((a, b) => b.time - a.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+246
-32
@@ -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,
|
||||
AuthInfo,
|
||||
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,18 @@ export default class SystemService {
|
||||
|
||||
public async getSystemConfig() {
|
||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||
return doc || {};
|
||||
return doc || ({} as SystemInstance);
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
await AuthModel.upsert({ ...payload });
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<SystemInstance> {
|
||||
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<SystemInstance> {
|
||||
const doc: any = await SystemModel.findOne({ where: { ...query } });
|
||||
return doc && doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||
@@ -63,32 +79,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 +306,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 +332,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) {
|
||||
@@ -193,6 +358,9 @@ export default class SystemService {
|
||||
this.scheduleService.runTask(
|
||||
`real_log_path=${logPath} real_time=true ${command}`,
|
||||
callback,
|
||||
{
|
||||
command,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,4 +385,50 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
+62
-40
@@ -5,18 +5,28 @@ import {
|
||||
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,
|
||||
AuthInfo,
|
||||
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 +37,7 @@ export default class UserService {
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private scheduleService: ScheduleService,
|
||||
private sockService: SockService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
public async login(
|
||||
payloads: {
|
||||
@@ -37,12 +47,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 +106,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 +137,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 +165,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 +198,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 },
|
||||
});
|
||||
}
|
||||
@@ -202,12 +224,12 @@ export default class UserService {
|
||||
}
|
||||
|
||||
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
|
||||
const doc = await AuthModel.create({ ...payload }, { returning: true });
|
||||
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 +252,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 +266,7 @@ export default class UserService {
|
||||
public async getUserInfo(): Promise<any> {
|
||||
const authFileExist = await fileExist(config.authConfigFile);
|
||||
if (!authFileExist) {
|
||||
fs.writeFileSync(
|
||||
await fs.writeFile(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
@@ -252,19 +274,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 +305,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 +327,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 +337,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 }),
|
||||
);
|
||||
@@ -333,21 +355,21 @@ export default class UserService {
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
let doc = await AuthModel.findOne({ type: payload.type });
|
||||
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 } });
|
||||
const doc: any = await SystemModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as any);
|
||||
}
|
||||
|
||||
|
||||
+88
-15
@@ -1,36 +1,109 @@
|
||||
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),
|
||||
});
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-16
@@ -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, () => {
|
||||
Logger.debug(`✌️ 更新服务启动成功!`);
|
||||
console.debug(`✌️ 更新服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
@@ -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,11 +1,10 @@
|
||||
#!/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
|
||||
make_dir /run/nginx
|
||||
@@ -16,34 +15,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',
|
||||
},
|
||||
],
|
||||
};
|
||||
+27
-14
@@ -4,13 +4,15 @@
|
||||
"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",
|
||||
@@ -58,7 +60,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,21 +70,21 @@
|
||||
"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",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
"protobufjs": "^7.2.3",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
@@ -95,7 +96,11 @@
|
||||
"typedi": "^0.10.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.6.0",
|
||||
"yargs": "^17.3.1"
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"yargs": "^17.3.1",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"request-ip": "3.3.0",
|
||||
"ip2region": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
@@ -108,6 +113,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 +130,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",
|
||||
"typescript": "5.2.2",
|
||||
"vh-check": "^2.0.5",
|
||||
"virtualizedtableforantd4": "1.3.0",
|
||||
"webpack": "^5.70.0",
|
||||
|
||||
Generated
+1584
-606
File diff suppressed because it is too large
Load Diff
+33
-18
@@ -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
|
||||
@@ -155,12 +148,18 @@ export AIBOTK_TYPE=""
|
||||
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
|
||||
export AIBOTK_NAME=""
|
||||
|
||||
## 13. CHRONOCAT
|
||||
## CHRONOCAT_URL 推送 http://127.0.0.1:16530
|
||||
## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
|
||||
## CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
|
||||
## CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
|
||||
export CHRONOCAT_URL=""
|
||||
export CHRONOCAT_QQ=""
|
||||
export CHRONOCAT_TOKEN=""
|
||||
|
||||
## 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"
|
||||
## 邮箱服务名称,比如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 +167,20 @@ export SMTP_PASSWORD=""
|
||||
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
|
||||
export SMTP_NAME=""
|
||||
|
||||
## 15. PushMe
|
||||
## 官方说明文档:https://push.i-i.me/
|
||||
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
|
||||
export PUSHME_KEY=""
|
||||
|
||||
## 15. 自定义通知
|
||||
## 自定义通知 接收回调的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 变量名= 声明即可
|
||||
|
||||
+421
-111
@@ -11,78 +11,83 @@
|
||||
*/
|
||||
|
||||
const querystring = require('querystring');
|
||||
const got = require('got');
|
||||
const $ = new Env();
|
||||
const timeout = 15000; //超时时间(单位毫秒)
|
||||
const timeout = 15000; // 超时时间(单位毫秒)
|
||||
// =======================================gotify通知设置区域==============================================
|
||||
//gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||
//gotify_token 填写gotify的消息应用token
|
||||
//gotify_priority 填写推送消息优先级,默认为0
|
||||
// gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||
// gotify_token 填写gotify的消息应用token
|
||||
// gotify_priority 填写推送消息优先级,默认为0
|
||||
let GOTIFY_URL = '';
|
||||
let GOTIFY_TOKEN = '';
|
||||
let GOTIFY_PRIORITY = 0;
|
||||
// =======================================go-cqhttp通知设置区域===========================================
|
||||
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg
|
||||
//gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||
//gobot_qq 填写推送到个人QQ或者QQ群号
|
||||
//go-cqhttp相关API https://docs.go-cqhttp.org/api
|
||||
// gobot_url 填写请求地址http://127.0.0.1/send_private_msg
|
||||
// gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||
// gobot_qq 填写推送到个人QQ或者QQ群号
|
||||
// go-cqhttp相关API https://docs.go-cqhttp.org/api
|
||||
let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||
let GOBOT_TOKEN = ''; //访问密钥
|
||||
let GOBOT_TOKEN = ''; // 访问密钥
|
||||
let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
||||
|
||||
// =======================================微信server酱通知设置区域===========================================
|
||||
//此处填你申请的SCKEY.
|
||||
//(环境变量名 PUSH_KEY)
|
||||
// 此处填你申请的SCKEY.
|
||||
// (环境变量名 PUSH_KEY)
|
||||
let SCKEY = '';
|
||||
|
||||
// =======================================PushDeer通知设置区域===========================================
|
||||
//此处填你申请的PushDeer KEY.
|
||||
//(环境变量名 DEER_KEY)
|
||||
// 此处填你申请的PushDeer KEY.
|
||||
// (环境变量名 DEER_KEY)
|
||||
let PUSHDEER_KEY = '';
|
||||
let PUSHDEER_URL = '';
|
||||
|
||||
// =======================================Synology Chat通知设置区域===========================================
|
||||
//此处填你申请的CHAT_URL与CHAT_TOKEN
|
||||
//(环境变量名 CHAT_URL CHAT_TOKEN)
|
||||
// 此处填你申请的CHAT_URL与CHAT_TOKEN
|
||||
// (环境变量名 CHAT_URL CHAT_TOKEN)
|
||||
let CHAT_URL = '';
|
||||
let CHAT_TOKEN = '';
|
||||
|
||||
// =======================================Bark App通知设置区域===========================================
|
||||
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
|
||||
// 此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
|
||||
let BARK_PUSH = '';
|
||||
//BARK app推送图标,自定义推送图标(需iOS15或以上)
|
||||
// BARK app推送图标,自定义推送图标(需iOS15或以上)
|
||||
let BARK_ICON = 'https://qn.whyour.cn/logo.png';
|
||||
//BARK app推送铃声,铃声列表去APP查看复制填写
|
||||
// BARK app推送铃声,铃声列表去APP查看复制填写
|
||||
let BARK_SOUND = '';
|
||||
//BARK app推送消息的分组, 默认为"QingLong"
|
||||
// BARK app推送消息的分组, 默认为"QingLong"
|
||||
let BARK_GROUP = 'QingLong';
|
||||
// BARK app推送消息的时效性, 默认为"active"
|
||||
let BARK_LEVEL = 'active';
|
||||
// BARK app推送消息的跳转URL
|
||||
let BARK_URL = '';
|
||||
|
||||
// =======================================telegram机器人通知设置区域===========================================
|
||||
//此处填你telegram bot 的Token,telegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
|
||||
//(环境变量名 TG_BOT_TOKEN)
|
||||
// 此处填你telegram bot 的Token,telegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
|
||||
// (环境变量名 TG_BOT_TOKEN)
|
||||
let TG_BOT_TOKEN = '';
|
||||
//此处填你接收通知消息的telegram用户的id,telegram机器人通知推送必填项.例如:129xxx206
|
||||
//(环境变量名 TG_USER_ID)
|
||||
// 此处填你接收通知消息的telegram用户的id,telegram机器人通知推送必填项.例如:129xxx206
|
||||
// (环境变量名 TG_USER_ID)
|
||||
let TG_USER_ID = '';
|
||||
//tg推送HTTP代理设置(不懂可忽略,telegram机器人通知推送功能中非必填)
|
||||
let TG_PROXY_HOST = ''; //例如:127.0.0.1(环境变量名:TG_PROXY_HOST)
|
||||
let TG_PROXY_PORT = ''; //例如:1080(环境变量名:TG_PROXY_PORT)
|
||||
let TG_PROXY_AUTH = ''; //tg代理配置认证参数
|
||||
//Telegram api自建的反向代理地址(不懂可忽略,telegram机器人通知推送功能中非必填),默认tg官方api(环境变量名:TG_API_HOST)
|
||||
let TG_API_HOST = 'api.telegram.org';
|
||||
// tg推送HTTP代理设置(不懂可忽略,telegram机器人通知推送功能中非必填)
|
||||
let TG_PROXY_HOST = ''; // 例如:127.0.0.1(环境变量名:TG_PROXY_HOST)
|
||||
let TG_PROXY_PORT = ''; // 例如:1080(环境变量名:TG_PROXY_PORT)
|
||||
let TG_PROXY_AUTH = ''; // tg代理配置认证参数
|
||||
// Telegram api自建的反向代理地址(不懂可忽略,telegram机器人通知推送功能中非必填),默认tg官方api(环境变量名:TG_API_HOST)
|
||||
let TG_API_HOST = 'https://api.telegram.org';
|
||||
// =======================================钉钉机器人通知设置区域===========================================
|
||||
//此处填你钉钉 bot 的webhook,例如:5a544165465465645d0f31dca676e7bd07415asdasd
|
||||
//(环境变量名 DD_BOT_TOKEN)
|
||||
// 此处填你钉钉 bot 的webhook,例如:5a544165465465645d0f31dca676e7bd07415asdasd
|
||||
// (环境变量名 DD_BOT_TOKEN)
|
||||
let DD_BOT_TOKEN = '';
|
||||
//密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串
|
||||
// 密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串
|
||||
let DD_BOT_SECRET = '';
|
||||
|
||||
// =======================================企业微信基础设置===========================================
|
||||
// 企业微信反向代理地址
|
||||
//(环境变量名 QYWX_ORIGIN)
|
||||
// (环境变量名 QYWX_ORIGIN)
|
||||
let QYWX_ORIGIN = '';
|
||||
// =======================================企业微信机器人通知设置区域===========================================
|
||||
//此处填你企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa
|
||||
//(环境变量名 QYWX_KEY)
|
||||
// 此处填你企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa
|
||||
// (环境变量名 QYWX_KEY)
|
||||
let QYWX_KEY = '';
|
||||
|
||||
// =======================================企业微信应用消息通知设置区域===========================================
|
||||
@@ -98,50 +103,63 @@ let QYWX_KEY = '';
|
||||
let QYWX_AM = '';
|
||||
|
||||
// =======================================iGot聚合推送通知设置区域===========================================
|
||||
//此处填您iGot的信息(推送key,例如:https://push.hellyw.com/XXXXXXXX)
|
||||
// 此处填您iGot的信息(推送key,例如:https://push.hellyw.com/XXXXXXXX)
|
||||
let IGOT_PUSH_KEY = '';
|
||||
|
||||
// =======================================push+设置区域=======================================
|
||||
//官方文档:http://www.pushplus.plus/
|
||||
//PUSH_PLUS_TOKEN:微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送
|
||||
//PUSH_PLUS_USER: 一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)
|
||||
// 官方文档:http://www.pushplus.plus/
|
||||
// PUSH_PLUS_TOKEN:微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送
|
||||
// PUSH_PLUS_USER: 一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)
|
||||
let PUSH_PLUS_TOKEN = '';
|
||||
let PUSH_PLUS_USER = '';
|
||||
|
||||
// =======================================Cool Push设置区域=======================================
|
||||
//官方文档:https://cp.xuthus.cc/docs
|
||||
//QQ_SKEY: Cool Push登录授权后推送消息的调用代码Skey
|
||||
//QQ_MODE: 推送模式详情请登录获取QQ_SKEY后见https://cp.xuthus.cc/feat
|
||||
let QQ_SKEY = '';
|
||||
let QQ_MODE = '';
|
||||
|
||||
// =======================================智能微秘书设置区域=======================================
|
||||
//官方文档:http://wechat.aibotk.com/docs/about
|
||||
//AIBOTK_KEY: 填写智能微秘书个人中心的apikey
|
||||
//AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
|
||||
//AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
|
||||
// 官方文档:http://wechat.aibotk.com/docs/about
|
||||
// AIBOTK_KEY: 填写智能微秘书个人中心的apikey
|
||||
// AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
|
||||
// AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
|
||||
let AIBOTK_KEY = '';
|
||||
let AIBOTK_TYPE = '';
|
||||
let AIBOTK_NAME = '';
|
||||
|
||||
// =======================================飞书机器人设置区域=======================================
|
||||
//官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
|
||||
//FSKEY 飞书机器人的 FSKEY
|
||||
// 官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
|
||||
// FSKEY 飞书机器人的 FSKEY
|
||||
let FSKEY = '';
|
||||
|
||||
// =======================================SMTP 邮件设置区域=======================================
|
||||
// SMTP_SERVER: 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||
// SMTP_SSL: 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
|
||||
// SMTP_SERVICE: 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
|
||||
// SMTP_EMAIL: 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
||||
// SMTP_PASSWORD: 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||
// SMTP_NAME: 填写 SMTP 收发件人姓名,可随意填写
|
||||
let SMTP_SERVER = '';
|
||||
let SMTP_SSL = 'false';
|
||||
let SMTP_SERVICE = '';
|
||||
let SMTP_EMAIL = '';
|
||||
let SMTP_PASSWORD = '';
|
||||
let SMTP_NAME = '';
|
||||
|
||||
//==========================云端环境变量的判断与接收=========================
|
||||
// =======================================PushMe通知设置区域===========================================
|
||||
// 官方文档:https://push.i-i.me/
|
||||
// 此处填你的PushMe KEY.
|
||||
let PUSHME_KEY = '';
|
||||
|
||||
// =======================================CHRONOCAT通知设置区域===========================================
|
||||
// CHRONOCAT_URL Red协议连接地址 例: http://127.0.0.1:16530
|
||||
// CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
|
||||
// CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群
|
||||
// CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
|
||||
let CHRONOCAT_URL = ''; // CHRONOCAT Red协议连接地址
|
||||
let CHRONOCAT_TOKEN = ''; // CHRONOCAT 生成的访问密钥
|
||||
let CHRONOCAT_QQ = ''; // 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
|
||||
|
||||
// =======================================自定义通知设置区域=======================================
|
||||
// 自定义通知 接收回调的URL
|
||||
let WEBHOOK_URL = '';
|
||||
let WEBHOOK_BODY = '';
|
||||
let WEBHOOK_HEADERS = '';
|
||||
let WEBHOOK_METHOD = '';
|
||||
let WEBHOOK_CONTENT_TYPE = '';
|
||||
|
||||
// ==========================云端环境变量的判断与接收=========================
|
||||
if (process.env.GOTIFY_URL) {
|
||||
GOTIFY_URL = process.env.GOTIFY_URL;
|
||||
}
|
||||
@@ -192,7 +210,7 @@ if (process.env.BARK_PUSH) {
|
||||
process.env.BARK_PUSH.indexOf('https') > -1 ||
|
||||
process.env.BARK_PUSH.indexOf('http') > -1
|
||||
) {
|
||||
//兼容BARK自建用户
|
||||
// 兼容BARK自建用户
|
||||
BARK_PUSH = process.env.BARK_PUSH;
|
||||
} else {
|
||||
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
|
||||
@@ -206,13 +224,19 @@ if (process.env.BARK_PUSH) {
|
||||
if (process.env.BARK_GROUP) {
|
||||
BARK_GROUP = process.env.BARK_GROUP;
|
||||
}
|
||||
if (process.env.BARK_LEVEL) {
|
||||
BARK_LEVEL = process.env.BARK_LEVEL;
|
||||
}
|
||||
if (process.env.BARK_URL) {
|
||||
BARK_URL = process.env.BARK_URL;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
BARK_PUSH &&
|
||||
BARK_PUSH.indexOf('https') === -1 &&
|
||||
BARK_PUSH.indexOf('http') === -1
|
||||
) {
|
||||
//兼容BARK本地用户只填写设备码的情况
|
||||
// 兼容BARK本地用户只填写设备码的情况
|
||||
BARK_PUSH = `https://api.day.app/${BARK_PUSH}`;
|
||||
}
|
||||
}
|
||||
@@ -273,11 +297,8 @@ if (process.env.FSKEY) {
|
||||
FSKEY = process.env.FSKEY;
|
||||
}
|
||||
|
||||
if (process.env.SMTP_SERVER) {
|
||||
SMTP_SERVER = process.env.SMTP_SERVER;
|
||||
}
|
||||
if (process.env.SMTP_SSL) {
|
||||
SMTP_SSL = process.env.SMTP_SSL;
|
||||
if (process.env.SMTP_SERVICE) {
|
||||
SMTP_SERVICE = process.env.SMTP_SERVICE;
|
||||
}
|
||||
if (process.env.SMTP_EMAIL) {
|
||||
SMTP_EMAIL = process.env.SMTP_EMAIL;
|
||||
@@ -288,7 +309,36 @@ if (process.env.SMTP_PASSWORD) {
|
||||
if (process.env.SMTP_NAME) {
|
||||
SMTP_NAME = process.env.SMTP_NAME;
|
||||
}
|
||||
//==========================云端环境变量的判断与接收=========================
|
||||
if (process.env.PUSHME_KEY) {
|
||||
PUSHME_KEY = process.env.PUSHME_KEY;
|
||||
}
|
||||
|
||||
if (process.env.CHRONOCAT_URL) {
|
||||
CHRONOCAT_URL = process.env.CHRONOCAT_URL;
|
||||
}
|
||||
if (process.env.CHRONOCAT_QQ) {
|
||||
CHRONOCAT_QQ = process.env.CHRONOCAT_QQ;
|
||||
}
|
||||
if (process.env.CHRONOCAT_TOKEN) {
|
||||
CHRONOCAT_TOKEN = process.env.CHRONOCAT_TOKEN;
|
||||
}
|
||||
|
||||
if (process.env.WEBHOOK_URL) {
|
||||
WEBHOOK_URL = process.env.WEBHOOK_URL;
|
||||
}
|
||||
if (process.env.WEBHOOK_BODY) {
|
||||
WEBHOOK_BODY = process.env.WEBHOOK_BODY;
|
||||
}
|
||||
if (process.env.WEBHOOK_HEADERS) {
|
||||
WEBHOOK_HEADERS = process.env.WEBHOOK_HEADERS;
|
||||
}
|
||||
if (process.env.WEBHOOK_METHOD) {
|
||||
WEBHOOK_METHOD = process.env.WEBHOOK_METHOD;
|
||||
}
|
||||
if (process.env.WEBHOOK_CONTENT_TYPE) {
|
||||
WEBHOOK_CONTENT_TYPE = process.env.WEBHOOK_CONTENT_TYPE;
|
||||
}
|
||||
// ==========================云端环境变量的判断与接收=========================
|
||||
|
||||
/**
|
||||
* sendNotify 推送通知功能
|
||||
@@ -304,8 +354,8 @@ async function sendNotify(
|
||||
params = {},
|
||||
author = '\n\n本通知 By:https://github.com/whyour/qinglong',
|
||||
) {
|
||||
//提供6种通知
|
||||
desp += author; //增加作者信息,防止被贩卖等
|
||||
// 提供6种通知
|
||||
desp += author; // 增加作者信息,防止被贩卖等
|
||||
|
||||
// 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
|
||||
let skipTitle = process.env.SKIP_PUSH_TITLE;
|
||||
@@ -317,25 +367,28 @@ async function sendNotify(
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
serverNotify(text, desp), //微信server酱
|
||||
pushPlusNotify(text, desp), //pushplus(推送加)
|
||||
serverNotify(text, desp), // 微信server酱
|
||||
pushPlusNotify(text, desp), // pushplus(推送加)
|
||||
]);
|
||||
//由于上述两种微信通知需点击进去才能查看到详情,故text(标题内容)携带了账号序号以及昵称信息,方便不点击也可知道是哪个京东哪个活动
|
||||
// 由于上述两种微信通知需点击进去才能查看到详情,故text(标题内容)携带了账号序号以及昵称信息,方便不点击也可知道是哪个京东哪个活动
|
||||
text = text.match(/.*?(?=\s?-)/g) ? text.match(/.*?(?=\s?-)/g)[0] : text;
|
||||
await Promise.all([
|
||||
BarkNotify(text, desp, params), //iOS Bark APP
|
||||
tgBotNotify(text, desp), //telegram 机器人
|
||||
ddBotNotify(text, desp), //钉钉机器人
|
||||
qywxBotNotify(text, desp), //企业微信机器人
|
||||
qywxamNotify(text, desp), //企业微信应用消息推送
|
||||
iGotNotify(text, desp, params), //iGot
|
||||
gobotNotify(text, desp), //go-cqhttp
|
||||
gotifyNotify(text, desp), //gotify
|
||||
ChatNotify(text, desp), //synolog chat
|
||||
PushDeerNotify(text, desp), //PushDeer
|
||||
aibotkNotify(text, desp), //智能微秘书
|
||||
fsBotNotify(text, desp), //飞书机器人
|
||||
smtpNotify(text, desp), //SMTP 邮件
|
||||
BarkNotify(text, desp, params), // iOS Bark APP
|
||||
tgBotNotify(text, desp), // telegram 机器人
|
||||
ddBotNotify(text, desp), // 钉钉机器人
|
||||
qywxBotNotify(text, desp), // 企业微信机器人
|
||||
qywxamNotify(text, desp), // 企业微信应用消息推送
|
||||
iGotNotify(text, desp, params), // iGot
|
||||
gobotNotify(text, desp), // go-cqhttp
|
||||
gotifyNotify(text, desp), // gotify
|
||||
ChatNotify(text, desp), // synolog chat
|
||||
PushDeerNotify(text, desp), // PushDeer
|
||||
aibotkNotify(text, desp), // 智能微秘书
|
||||
fsBotNotify(text, desp), // 飞书机器人
|
||||
smtpNotify(text, desp), // SMTP 邮件
|
||||
pushMeNotify(text, desp, params), // PushMe
|
||||
chronocatNotify(text, desp), // Chronocat
|
||||
webhookNotify(text, desp), // 自定义通知
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -417,7 +470,7 @@ function gobotNotify(text, desp) {
|
||||
function serverNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
if (SCKEY) {
|
||||
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
|
||||
// 微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
|
||||
desp = desp.replace(/[\n\r]/g, '\n\n');
|
||||
const options = {
|
||||
url: SCKEY.includes('SCT')
|
||||
@@ -436,7 +489,7 @@ function serverNotify(text, desp) {
|
||||
console.log(err);
|
||||
} else {
|
||||
data = JSON.parse(data);
|
||||
//server酱和Server酱·Turbo版的返回json格式不太一样
|
||||
// server酱和Server酱·Turbo版的返回json格式不太一样
|
||||
if (data.errno === 0 || data.data.errno === 0) {
|
||||
console.log('server酱发送通知消息成功🎉\n');
|
||||
} else if (data.errno === 1024) {
|
||||
@@ -543,7 +596,7 @@ function BarkNotify(text, desp, params = {}) {
|
||||
const options = {
|
||||
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
|
||||
desp,
|
||||
)}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
|
||||
)}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&level=${BARK_LEVEL}&url=${BARK_URL}&${querystring.stringify(
|
||||
params,
|
||||
)}`,
|
||||
headers: {
|
||||
@@ -580,7 +633,7 @@ function tgBotNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
if (TG_BOT_TOKEN && TG_USER_ID) {
|
||||
const options = {
|
||||
url: `https://${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
|
||||
url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
|
||||
json: {
|
||||
chat_id: `${TG_USER_ID}`,
|
||||
text: `${text}\n\n${desp}`,
|
||||
@@ -592,15 +645,19 @@ function tgBotNotify(text, desp) {
|
||||
timeout,
|
||||
};
|
||||
if (TG_PROXY_HOST && TG_PROXY_PORT) {
|
||||
const tunnel = require('tunnel');
|
||||
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
|
||||
const options = {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 256,
|
||||
maxFreeSockets: 256,
|
||||
proxy: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
|
||||
};
|
||||
const httpAgent = new HttpProxyAgent(options);
|
||||
const httpsAgent = new HttpsProxyAgent(options);
|
||||
const agent = {
|
||||
https: tunnel.httpsOverHttp({
|
||||
proxy: {
|
||||
host: TG_PROXY_HOST,
|
||||
port: TG_PROXY_PORT * 1,
|
||||
proxyAuth: TG_PROXY_AUTH,
|
||||
},
|
||||
}),
|
||||
http: httpAgent,
|
||||
https: httpsAgent,
|
||||
};
|
||||
Object.assign(options, { agent });
|
||||
}
|
||||
@@ -759,7 +816,18 @@ function ChangeUserId(desp) {
|
||||
}
|
||||
}
|
||||
|
||||
function qywxamNotify(text, desp) {
|
||||
async function qywxamNotify(text, desp) {
|
||||
const MAX_LENGTH = 900;
|
||||
if (desp.length > MAX_LENGTH) {
|
||||
let d = desp.substr(0, MAX_LENGTH) + '\n==More==';
|
||||
await do_qywxamNotify(text, d);
|
||||
await qywxamNotify(text, desp.substr(MAX_LENGTH));
|
||||
} else {
|
||||
return await do_qywxamNotify(text, desp);
|
||||
}
|
||||
}
|
||||
|
||||
function do_qywxamNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
if (QYWX_AM) {
|
||||
const QYWX_AM_AY = QYWX_AM.split(',');
|
||||
@@ -820,7 +888,7 @@ function qywxamNotify(text, desp) {
|
||||
};
|
||||
}
|
||||
if (!QYWX_AM_AY[4]) {
|
||||
//如不提供第四个参数,则默认进行文本消息类型推送
|
||||
// 如不提供第四个参数,则默认进行文本消息类型推送
|
||||
options = {
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
@@ -1072,31 +1140,31 @@ function fsBotNotify(text, desp) {
|
||||
}
|
||||
|
||||
async function smtpNotify(text, desp) {
|
||||
if (![SMTP_SERVER, SMTP_EMAIL, SMTP_PASSWORD].every(Boolean)) {
|
||||
if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const nodemailer = require('nodemailer');
|
||||
const transporter = nodemailer.createTransport(
|
||||
`${SMTP_SSL === 'true' ? 'smtps:' : 'smtp:'}//${SMTP_SERVER}`,
|
||||
{
|
||||
auth: {
|
||||
user: SMTP_EMAIL,
|
||||
pass: SMTP_PASSWORD,
|
||||
},
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: SMTP_SERVICE,
|
||||
auth: {
|
||||
user: SMTP_EMAIL,
|
||||
pass: SMTP_PASSWORD,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
|
||||
const info = await transporter.sendMail({
|
||||
from: addr,
|
||||
to: addr,
|
||||
subject: text,
|
||||
text: desp,
|
||||
html: `${desp.replace(/\n/g, '<br/>')}`,
|
||||
});
|
||||
|
||||
if (!!info.messageId) {
|
||||
transporter.close();
|
||||
|
||||
if (info.messageId) {
|
||||
console.log('SMTP发送通知消息成功🎉\n');
|
||||
return true;
|
||||
}
|
||||
@@ -1107,16 +1175,258 @@ async function smtpNotify(text, desp) {
|
||||
}
|
||||
}
|
||||
|
||||
function smtpNotify(text, desp) {
|
||||
function pushMeNotify(text, desp, params = {}) {
|
||||
return new Promise((resolve) => {
|
||||
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
|
||||
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
|
||||
if (PUSHME_KEY) {
|
||||
const options = {
|
||||
url: `https://push.i-i.me?push_key=${PUSHME_KEY}`,
|
||||
json: { title: text, content: desp, ...params },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout,
|
||||
};
|
||||
$.post(options, (err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('PushMeNotify发送通知调用API失败!!\n');
|
||||
console.log(err);
|
||||
} else {
|
||||
if (data === 'success') {
|
||||
console.log('PushMe发送通知消息成功🎉\n');
|
||||
} else {
|
||||
console.log(`${data}\n`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$.logErr(e, resp);
|
||||
} finally {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function chronocatNotify(title, desp) {
|
||||
return new Promise((resolve) => {
|
||||
if (!CHRONOCAT_TOKEN || !CHRONOCAT_QQ || !CHRONOCAT_URL) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const user_ids = CHRONOCAT_QQ.match(/user_id=(\d+)/g)?.map(
|
||||
(match) => match.split('=')[1],
|
||||
);
|
||||
const group_ids = CHRONOCAT_QQ.match(/group_id=(\d+)/g)?.map(
|
||||
(match) => match.split('=')[1],
|
||||
);
|
||||
|
||||
const url = `${CHRONOCAT_URL}/api/message/send`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${CHRONOCAT_TOKEN}`,
|
||||
};
|
||||
|
||||
for (const [chat_type, ids] of [
|
||||
[1, user_ids],
|
||||
[2, group_ids],
|
||||
]) {
|
||||
if (!ids) {
|
||||
continue;
|
||||
}
|
||||
for (const chat_id of ids) {
|
||||
const data = {
|
||||
peer: {
|
||||
chatType: chat_type,
|
||||
peerUin: chat_id,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
elementType: 1,
|
||||
textElement: {
|
||||
content: `${title}\n\n${desp}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const options = {
|
||||
url: url,
|
||||
json: data,
|
||||
headers,
|
||||
timeout,
|
||||
};
|
||||
$.post(options, (err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('Chronocat发送QQ通知消息失败!!\n');
|
||||
console.log(err);
|
||||
} else {
|
||||
data = JSON.parse(data);
|
||||
if (chat_type === 1) {
|
||||
console.log(`QQ个人消息:${ids}推送成功!`);
|
||||
} else {
|
||||
console.log(`QQ群消息:${ids}推送成功!`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$.logErr(e, resp);
|
||||
} finally {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function webhookNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
if (!WEBHOOK_URL.includes('$title') && !WEBHOOK_BODY.includes('$title')) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = parseHeaders(WEBHOOK_HEADERS);
|
||||
const body = parseBody(WEBHOOK_BODY, WEBHOOK_CONTENT_TYPE, (v) =>
|
||||
v?.replaceAll('$title', text)?.replaceAll('$content', desp),
|
||||
);
|
||||
const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);
|
||||
const options = {
|
||||
method: WEBHOOK_METHOD,
|
||||
headers,
|
||||
allowGetBody: true,
|
||||
...bodyParam,
|
||||
timeout,
|
||||
retry: 1,
|
||||
};
|
||||
|
||||
if (WEBHOOK_METHOD) {
|
||||
const formatUrl = WEBHOOK_URL.replaceAll(
|
||||
'$title',
|
||||
encodeURIComponent(text),
|
||||
).replaceAll('$content', encodeURIComponent(desp));
|
||||
got(formatUrl, options).then((resp) => {
|
||||
try {
|
||||
if (resp.statusCode !== 200) {
|
||||
console.log(`自定义发送通知消息失败!!\n${resp.body}`);
|
||||
} else {
|
||||
console.log(`自定义发送通知消息成功🎉。\n${resp.body}`);
|
||||
}
|
||||
} catch (e) {
|
||||
$.logErr(e, resp);
|
||||
} finally {
|
||||
resolve(resp.body);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseString(input, valueFormatFn) {
|
||||
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
|
||||
const matches = {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function parseHeaders(headers) {
|
||||
if (!headers) return {};
|
||||
|
||||
const parsed = {};
|
||||
let key;
|
||||
let val;
|
||||
let i;
|
||||
|
||||
headers &&
|
||||
headers.split('\n').forEach(function parser(line) {
|
||||
i = line.indexOf(':');
|
||||
key = line.substring(0, i).trim().toLowerCase();
|
||||
val = line.substring(i + 1).trim();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseBody(body, contentType, valueFormatFn) {
|
||||
if (contentType === 'text/plain' || !body) {
|
||||
return body;
|
||||
}
|
||||
|
||||
const parsed = parseString(body, valueFormatFn);
|
||||
|
||||
switch (contentType) {
|
||||
case 'multipart/form-data':
|
||||
return Object.keys(parsed).reduce((p, c) => {
|
||||
p.append(c, parsed[c]);
|
||||
return p;
|
||||
}, new FormData());
|
||||
case 'application/x-www-form-urlencoded':
|
||||
return Object.keys(parsed).reduce((p, c) => {
|
||||
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
|
||||
});
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function formatBodyFun(contentType, body) {
|
||||
if (!body) return {};
|
||||
switch (contentType) {
|
||||
case 'application/json':
|
||||
return { json: body };
|
||||
case 'multipart/form-data':
|
||||
return { form: body };
|
||||
case 'application/x-www-form-urlencoded':
|
||||
return { body };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function formatNotifyContentFun(url, body, title, content) {
|
||||
if (!url.includes('$title') && !body.includes('$title')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
formatUrl: url
|
||||
.replaceAll('$title', encodeURIComponent(title))
|
||||
.replaceAll('$content', encodeURIComponent(content)),
|
||||
formatBody: body
|
||||
.replaceAll('$title', title)
|
||||
.replaceAll('$content', content),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendNotify,
|
||||
BARK_PUSH,
|
||||
|
||||
+501
-157
@@ -40,6 +40,8 @@ push_config = {
|
||||
'BARK_GROUP': '', # bark 推送分组
|
||||
'BARK_SOUND': '', # bark 推送声音
|
||||
'BARK_ICON': '', # bark 推送图标
|
||||
'BARK_LEVEL': '', # bark 推送时效性
|
||||
'BARK_URL': '', # bark 推送跳转URL
|
||||
|
||||
'CONSOLE': True, # 控制台输出
|
||||
|
||||
@@ -77,7 +79,7 @@ push_config = {
|
||||
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
||||
|
||||
'QYWX_ORIGIN': '', # 企业微信代理地址
|
||||
|
||||
|
||||
'QYWX_AM': '', # 企业微信应用
|
||||
|
||||
'QYWX_KEY': '', # 企业微信机器人
|
||||
@@ -98,6 +100,18 @@ push_config = {
|
||||
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
|
||||
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
||||
|
||||
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
|
||||
|
||||
'CHRONOCAT_QQ': '', # qq号
|
||||
'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token
|
||||
'CHRONOCAT_URL': '', # CHRONOCAT的url地址
|
||||
|
||||
'WEBHOOK_URL': '', # 自定义通知 请求地址
|
||||
'WEBHOOK_BODY': '', # 自定义通知 请求体
|
||||
'WEBHOOK_HEADERS': '', # 自定义通知 请求头
|
||||
'WEBHOOK_METHOD': '', # 自定义通知 请求方法
|
||||
'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type
|
||||
}
|
||||
notify_function = []
|
||||
# fmt: on
|
||||
@@ -109,35 +123,38 @@ for k in push_config:
|
||||
push_config[k] = v
|
||||
|
||||
|
||||
def bark(title: str, content: str) -> None:
|
||||
def bark(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 bark 推送消息。
|
||||
"""
|
||||
if not push_config.get("BARK_PUSH"):
|
||||
if not (push_config.get("BARK_PUSH") or kwargs.get("BARK_PUSH")):
|
||||
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
|
||||
return
|
||||
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)}'
|
||||
BARK_PUSH = kwargs.get("BARK_PUSH", push_config.get("BARK_PUSH"))
|
||||
if BARK_PUSH.startswith("http"):
|
||||
url = f"{BARK_PUSH}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}"
|
||||
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/{BARK_PUSH}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}"
|
||||
|
||||
bark_params = {
|
||||
"BARK_ARCHIVE": "isArchive",
|
||||
"BARK_GROUP": "group",
|
||||
"BARK_SOUND": "sound",
|
||||
"BARK_ICON": "icon",
|
||||
"BARK_LEVEL": "level",
|
||||
"BARK_URL": "url",
|
||||
}
|
||||
params = ""
|
||||
for pair in filter(
|
||||
lambda pairs: pairs[0].startswith("BARK_")
|
||||
and pairs[0] != "BARK_PUSH"
|
||||
and pairs[1]
|
||||
and (pairs[1] or kwargs.get(pairs[0]))
|
||||
and bark_params.get(pairs[0]),
|
||||
push_config.items(),
|
||||
):
|
||||
params += f"{bark_params.get(pair[0])}={pair[1]}&"
|
||||
value = kwargs.get(pair[0], pair[1])
|
||||
params += f"{bark_params.get(pair[0])}={value}&"
|
||||
if params:
|
||||
url = url + "?" + params.rstrip("&")
|
||||
response = requests.get(url).json()
|
||||
@@ -148,31 +165,40 @@ def bark(title: str, content: str) -> None:
|
||||
print("bark 推送失败!")
|
||||
|
||||
|
||||
def console(title: str, content: str) -> None:
|
||||
def console(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 控制台 推送消息。
|
||||
"""
|
||||
print(f"{title}\n\n{content}")
|
||||
|
||||
|
||||
def dingding_bot(title: str, content: str) -> None:
|
||||
def dingding_bot(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 钉钉机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
|
||||
if not (
|
||||
(kwargs.get("DD_BOT_SECRET") and kwargs.get("DD_BOT_TOKEN"))
|
||||
or (push_config.get("DD_BOT_SECRET") and push_config.get("DD_BOT_TOKEN"))
|
||||
):
|
||||
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("钉钉机器人 服务启动")
|
||||
if kwargs.get("DD_BOT_SECRET") and kwargs.get("DD_BOT_TOKEN"):
|
||||
DD_BOT_SECRET = kwargs.get("DD_BOT_SECRET")
|
||||
DD_BOT_TOKEN = kwargs.get("DD_BOT_TOKEN")
|
||||
else:
|
||||
DD_BOT_SECRET = push_config.get("DD_BOT_SECRET")
|
||||
DD_BOT_TOKEN = push_config.get("DD_BOT_TOKEN")
|
||||
|
||||
timestamp = str(round(time.time() * 1000))
|
||||
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
|
||||
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
|
||||
secret_enc = DD_BOT_SECRET.encode("utf-8")
|
||||
string_to_sign = "{}\n{}".format(timestamp, DD_BOT_SECRET)
|
||||
string_to_sign_enc = string_to_sign.encode("utf-8")
|
||||
hmac_code = hmac.new(
|
||||
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
|
||||
).digest()
|
||||
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
||||
url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get("DD_BOT_TOKEN")}×tamp={timestamp}&sign={sign}'
|
||||
url = f"https://oapi.dingtalk.com/robot/send?access_token={DD_BOT_TOKEN}×tamp={timestamp}&sign={sign}"
|
||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||
response = requests.post(
|
||||
@@ -185,16 +211,16 @@ def dingding_bot(title: str, content: str) -> None:
|
||||
print("钉钉机器人 推送失败!")
|
||||
|
||||
|
||||
def feishu_bot(title: str, content: str) -> None:
|
||||
def feishu_bot(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 飞书机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("FSKEY"):
|
||||
if not (kwargs.get("DD_BOT_SECRET") or push_config.get("FSKEY")):
|
||||
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("飞书 服务启动")
|
||||
|
||||
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
|
||||
FSKEY = kwargs.get("DD_BOT_SECRET", push_config.get("FSKEY"))
|
||||
url = f"https://open.feishu.cn/open-apis/bot/v2/hook/{FSKEY}"
|
||||
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
|
||||
response = requests.post(url, data=json.dumps(data)).json()
|
||||
|
||||
@@ -204,16 +230,27 @@ def feishu_bot(title: str, content: str) -> None:
|
||||
print("飞书 推送失败!错误信息如下:\n", response)
|
||||
|
||||
|
||||
def go_cqhttp(title: str, content: str) -> None:
|
||||
def go_cqhttp(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 go_cqhttp 推送消息。
|
||||
"""
|
||||
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
|
||||
if not (
|
||||
(kwargs.get("GOBOT_URL") and kwargs.get("GOBOT_QQ"))
|
||||
or (push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"))
|
||||
):
|
||||
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
|
||||
return
|
||||
print("go-cqhttp 服务启动")
|
||||
if kwargs.get("GOBOT_URL") and kwargs.get("GOBOT_QQ"):
|
||||
GOBOT_URL = kwargs.get("GOBOT_URL")
|
||||
GOBOT_QQ = kwargs.get("GOBOT_QQ")
|
||||
GOBOT_TOKEN = kwargs.get("GOBOT_TOKEN")
|
||||
else:
|
||||
GOBOT_URL = push_config.get("GOBOT_URL")
|
||||
GOBOT_QQ = push_config.get("GOBOT_QQ")
|
||||
GOBOT_TOKEN = push_config.get("GOBOT_TOKEN")
|
||||
|
||||
url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
|
||||
url = f"{GOBOT_URL}?access_token={GOBOT_TOKEN}&{GOBOT_QQ}&message=标题:{title}\n内容:{content}"
|
||||
response = requests.get(url).json()
|
||||
|
||||
if response["status"] == "ok":
|
||||
@@ -222,20 +259,31 @@ def go_cqhttp(title: str, content: str) -> None:
|
||||
print("go-cqhttp 推送失败!")
|
||||
|
||||
|
||||
def gotify(title: str, content: str) -> None:
|
||||
def gotify(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 gotify 推送消息。
|
||||
"""
|
||||
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
||||
if not (
|
||||
(kwargs.get("GOTIFY_URL") and kwargs.get("GOTIFY_TOKEN"))
|
||||
or (push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"))
|
||||
):
|
||||
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("gotify 服务启动")
|
||||
if kwargs.get("GOTIFY_URL") and kwargs.get("GOTIFY_TOKEN"):
|
||||
GOTIFY_URL = kwargs.get("GOTIFY_URL")
|
||||
GOTIFY_TOKEN = kwargs.get("GOBOTGOTIFY_TOKEN_QQ")
|
||||
GOTIFY_PRIORITY = kwargs.get("GOTIFY_PRIORITY")
|
||||
else:
|
||||
GOTIFY_URL = push_config.get("GOTIFY_URL")
|
||||
GOTIFY_TOKEN = push_config.get("GOTIFY_TOKEN")
|
||||
GOTIFY_PRIORITY = kwargs.get("GOTIFY_PRIORITY")
|
||||
|
||||
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
|
||||
url = f"{GOTIFY_URL}/message?token={GOTIFY_TOKEN}"
|
||||
data = {
|
||||
"title": title,
|
||||
"message": content,
|
||||
"priority": push_config.get("GOTIFY_PRIORITY"),
|
||||
"priority": GOTIFY_PRIORITY,
|
||||
}
|
||||
response = requests.post(url, data=data).json()
|
||||
|
||||
@@ -245,16 +293,16 @@ def gotify(title: str, content: str) -> None:
|
||||
print("gotify 推送失败!")
|
||||
|
||||
|
||||
def iGot(title: str, content: str) -> None:
|
||||
def iGot(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 iGot 推送消息。
|
||||
"""
|
||||
if not push_config.get("IGOT_PUSH_KEY"):
|
||||
if not (kwargs.get("IGOT_PUSH_KEY") or push_config.get("IGOT_PUSH_KEY")):
|
||||
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("iGot 服务启动")
|
||||
|
||||
url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
|
||||
IGOT_PUSH_KEY = kwargs.get("IGOT_PUSH_KEY", push_config.get("IGOT_PUSH_KEY"))
|
||||
url = f"https://push.hellyw.com/{IGOT_PUSH_KEY}"
|
||||
data = {"title": title, "content": content}
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
response = requests.post(url, data=data, headers=headers).json()
|
||||
@@ -265,20 +313,21 @@ def iGot(title: str, content: str) -> None:
|
||||
print(f'iGot 推送失败!{response["errMsg"]}')
|
||||
|
||||
|
||||
def serverJ(title: str, content: str) -> None:
|
||||
def serverJ(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
通过 serverJ 推送消息。
|
||||
"""
|
||||
if not push_config.get("PUSH_KEY"):
|
||||
if not (kwargs.get("PUSH_KEY") or push_config.get("PUSH_KEY")):
|
||||
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("serverJ 服务启动")
|
||||
PUSH_KEY = kwargs.get("PUSH_KEY", push_config.get("PUSH_KEY"))
|
||||
|
||||
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
||||
if push_config.get("PUSH_KEY").find("SCT") != -1:
|
||||
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||
if PUSH_KEY.find("SCT") != -1:
|
||||
url = f"https://sctapi.ftqq.com/{PUSH_KEY}.send"
|
||||
else:
|
||||
url = f'https://sc.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||
url = f"https://sc.ftqq.com/{PUSH_KEY}.send"
|
||||
response = requests.post(url, data=data).json()
|
||||
|
||||
if response.get("errno") == 0 or response.get("code") == 0:
|
||||
@@ -287,23 +336,27 @@ def serverJ(title: str, content: str) -> None:
|
||||
print(f'serverJ 推送失败!错误码:{response["message"]}')
|
||||
|
||||
|
||||
def pushdeer(title: str, content: str) -> None:
|
||||
def pushdeer(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
通过PushDeer 推送消息
|
||||
"""
|
||||
if not push_config.get("DEER_KEY"):
|
||||
if not (kwargs.get("DEER_KEY") or push_config.get("DEER_KEY")):
|
||||
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("PushDeer 服务启动")
|
||||
DEER_KEY = kwargs.get("DEER_KEY", push_config.get("DEER_KEY"))
|
||||
|
||||
data = {
|
||||
"text": title,
|
||||
"desp": content,
|
||||
"type": "markdown",
|
||||
"pushkey": push_config.get("DEER_KEY"),
|
||||
"pushkey": DEER_KEY,
|
||||
}
|
||||
url = "https://api2.pushdeer.com/message/push"
|
||||
if push_config.get("DEER_URL"):
|
||||
url = push_config.get("DEER_URL")
|
||||
if kwargs.get("DEER_URL"):
|
||||
url = kwargs.get("DEER_URL")
|
||||
|
||||
response = requests.post(url, data=data).json()
|
||||
|
||||
@@ -313,16 +366,26 @@ def pushdeer(title: str, content: str) -> None:
|
||||
print("PushDeer 推送失败!错误信息:", response)
|
||||
|
||||
|
||||
def chat(title: str, content: str) -> None:
|
||||
def chat(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
通过Chat 推送消息
|
||||
"""
|
||||
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
|
||||
if not (
|
||||
(kwargs.get("CHAT_URL") and kwargs.get("CHAT_TOKEN"))
|
||||
or (push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"))
|
||||
):
|
||||
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("chat 服务启动")
|
||||
if kwargs.get("CHAT_URL") and kwargs.get("CHAT_TOKEN"):
|
||||
CHAT_URL = kwargs.get("CHAT_URL")
|
||||
CHAT_TOKEN = kwargs.get("CHAT_TOKEN")
|
||||
else:
|
||||
CHAT_URL = push_config.get("CHAT_URL")
|
||||
CHAT_TOKEN = push_config.get("CHAT_TOKEN")
|
||||
|
||||
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
||||
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
|
||||
url = CHAT_URL + CHAT_TOKEN
|
||||
response = requests.post(url, data=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -331,21 +394,23 @@ def chat(title: str, content: str) -> None:
|
||||
print("Chat 推送失败!错误信息:", response)
|
||||
|
||||
|
||||
def pushplus_bot(title: str, content: str) -> None:
|
||||
def pushplus_bot(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
通过 push+ 推送消息。
|
||||
"""
|
||||
if not push_config.get("PUSH_PLUS_TOKEN"):
|
||||
if not (kwargs.get("PUSH_PLUS_TOKEN") or push_config.get("PUSH_PLUS_TOKEN")):
|
||||
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("PUSHPLUS 服务启动")
|
||||
PUSH_PLUS_TOKEN = kwargs.get("PUSH_PLUS_TOKEN", push_config.get("PUSH_PLUS_TOKEN"))
|
||||
PUSH_PLUS_USER = kwargs.get("PUSH_PLUS_USER", push_config.get("PUSH_PLUS_USER"))
|
||||
|
||||
url = "http://www.pushplus.plus/send"
|
||||
data = {
|
||||
"token": push_config.get("PUSH_PLUS_TOKEN"),
|
||||
"token": PUSH_PLUS_TOKEN,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"topic": push_config.get("PUSH_PLUS_USER"),
|
||||
"topic": PUSH_PLUS_USER,
|
||||
}
|
||||
body = json.dumps(data).encode(encoding="utf-8")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
@@ -366,16 +431,25 @@ def pushplus_bot(title: str, content: str) -> None:
|
||||
print("PUSHPLUS 推送失败!")
|
||||
|
||||
|
||||
def qmsg_bot(title: str, content: str) -> None:
|
||||
def qmsg_bot(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 qmsg 推送消息。
|
||||
"""
|
||||
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
|
||||
if not (
|
||||
(kwargs.get("QMSG_KEY") and kwargs.get("QMSG_TYPE"))
|
||||
or (push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"))
|
||||
):
|
||||
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
|
||||
return
|
||||
print("qmsg 服务启动")
|
||||
if kwargs.get("QMSG_KEY") and kwargs.get("QMSG_TYPE"):
|
||||
QMSG_KEY = kwargs.get("QMSG_KEY")
|
||||
QMSG_TYPE = kwargs.get("QMSG_TYPE")
|
||||
else:
|
||||
QMSG_KEY = push_config.get("QMSG_KEY")
|
||||
QMSG_TYPE = push_config.get("QMSG_TYPE")
|
||||
|
||||
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
|
||||
url = f"https://qmsg.zendee.cn/{QMSG_TYPE}/{QMSG_KEY}"
|
||||
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
|
||||
response = requests.post(url=url, params=payload).json()
|
||||
|
||||
@@ -385,14 +459,15 @@ def qmsg_bot(title: str, content: str) -> None:
|
||||
print(f'qmsg 推送失败!{response["reason"]}')
|
||||
|
||||
|
||||
def wecom_app(title: str, content: str) -> None:
|
||||
def wecom_app(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
通过 企业微信 APP 推送消息。
|
||||
"""
|
||||
if not push_config.get("QYWX_AM"):
|
||||
if not (kwargs.get("QYWX_AM") or push_config.get("QYWX_AM")):
|
||||
print("QYWX_AM 未设置!!\n取消推送")
|
||||
return
|
||||
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
||||
QYWX_AM = kwargs.get("QYWX_AM", push_config.get("QYWX_AM"))
|
||||
QYWX_AM_AY = re.split(",", QYWX_AM)
|
||||
if 4 < len(QYWX_AM_AY) > 5:
|
||||
print("QYWX_AM 设置错误!!\n取消推送")
|
||||
return
|
||||
@@ -440,7 +515,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 +531,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",
|
||||
@@ -478,20 +557,23 @@ class WeCom:
|
||||
return respone["errmsg"]
|
||||
|
||||
|
||||
def wecom_bot(title: str, content: str) -> None:
|
||||
def wecom_bot(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
通过 企业微信机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("QYWX_KEY"):
|
||||
if not (kwargs.get("QYWX_KEY") or push_config.get("QYWX_KEY")):
|
||||
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("企业微信机器人服务启动")
|
||||
QYWX_KEY = kwargs.get("QYWX_KEY", push_config.get("QYWX_KEY"))
|
||||
|
||||
origin = "https://qyapi.weixin.qq.com"
|
||||
if push_config.get("QYWX_ORIGIN"):
|
||||
origin = push_config.get("QYWX_ORIGIN")
|
||||
if kwargs.get("QYWX_ORIGIN"):
|
||||
origin = kwargs.get("QYWX_ORIGIN")
|
||||
|
||||
url = f"{origin}/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
|
||||
url = f"{origin}/cgi-bin/webhook/send?key={QYWX_KEY}"
|
||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||
response = requests.post(
|
||||
@@ -504,40 +586,53 @@ def wecom_bot(title: str, content: str) -> None:
|
||||
print("企业微信机器人推送失败!")
|
||||
|
||||
|
||||
def telegram_bot(title: str, content: str) -> None:
|
||||
def telegram_bot(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 telegram 机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
|
||||
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
|
||||
if not (
|
||||
(kwargs.get("TG_BOT_TOKEN") and kwargs.get("TG_USER_ID"))
|
||||
or (push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"))
|
||||
):
|
||||
print("tg 服务的 TG_BOT_TOKEN 或者 TG_USER_ID 未设置!!\n取消推送")
|
||||
return
|
||||
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"
|
||||
if kwargs.get("TG_BOT_TOKEN") and kwargs.get("TG_USER_ID"):
|
||||
TG_BOT_TOKEN = kwargs.get("TG_BOT_TOKEN")
|
||||
TG_USER_ID = kwargs.get("TG_USER_ID")
|
||||
else:
|
||||
url = (
|
||||
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||
)
|
||||
TG_BOT_TOKEN = push_config.get("TG_BOT_TOKEN")
|
||||
TG_USER_ID = push_config.get("TG_USER_ID")
|
||||
|
||||
if kwargs.get("TG_API_HOST") or push_config.get("TG_API_HOST"):
|
||||
TG_API_HOST = kwargs.get("TG_API_HOST", push_config.get("TG_API_HOST"))
|
||||
url = f"{TG_API_HOST}/bot{TG_BOT_TOKEN}/sendMessage"
|
||||
else:
|
||||
url = f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage"
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
payload = {
|
||||
"chat_id": str(push_config.get("TG_USER_ID")),
|
||||
"chat_id": str(TG_USER_ID),
|
||||
"text": f"{title}\n\n{content}",
|
||||
"disable_web_page_preview": "true",
|
||||
}
|
||||
proxies = None
|
||||
if push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"):
|
||||
if push_config.get("TG_PROXY_AUTH") is not None and "@" not in push_config.get(
|
||||
"TG_PROXY_HOST"
|
||||
):
|
||||
push_config["TG_PROXY_HOST"] = (
|
||||
push_config.get("TG_PROXY_AUTH")
|
||||
+ "@"
|
||||
+ push_config.get("TG_PROXY_HOST")
|
||||
if not (
|
||||
(kwargs.get("TG_PROXY_HOST") and kwargs.get("TG_PROXY_PORT"))
|
||||
or (push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"))
|
||||
):
|
||||
if kwargs.get("TG_PROXY_HOST") and kwargs.get("TG_PROXY_PORT"):
|
||||
TG_PROXY_HOST = kwargs.get("TG_PROXY_HOST")
|
||||
TG_PROXY_PORT = kwargs.get("TG_PROXY_PORT")
|
||||
else:
|
||||
TG_PROXY_HOST = kwargs.get("TG_PROXY_HOST")
|
||||
TG_PROXY_PORT = kwargs.get("TG_PROXY_PORT")
|
||||
if kwargs.get("TG_PROXY_AUTH") or push_config.get("TG_PROXY_AUTH"):
|
||||
TG_PROXY_AUTH = kwargs.get(
|
||||
"TG_PROXY_AUTH", push_config.get("TG_PROXY_AUTH")
|
||||
)
|
||||
proxyStr = "http://{}:{}".format(
|
||||
push_config.get("TG_PROXY_HOST"), push_config.get("TG_PROXY_PORT")
|
||||
)
|
||||
if TG_PROXY_AUTH is not None and "@" not in TG_PROXY_HOST:
|
||||
TG_PROXY_HOST = TG_PROXY_AUTH + "@" + TG_PROXY_HOST
|
||||
proxyStr = "http://{}:{}".format(TG_PROXY_HOST, TG_PROXY_PORT)
|
||||
proxies = {"http": proxyStr, "https": proxyStr}
|
||||
response = requests.post(
|
||||
url=url, headers=headers, params=payload, proxies=proxies
|
||||
@@ -549,31 +644,51 @@ def telegram_bot(title: str, content: str) -> None:
|
||||
print("tg 推送失败!")
|
||||
|
||||
|
||||
def aibotk(title: str, content: str) -> None:
|
||||
def aibotk(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 智能微秘书 推送消息。
|
||||
"""
|
||||
if (
|
||||
not push_config.get("AIBOTK_KEY")
|
||||
or not push_config.get("AIBOTK_TYPE")
|
||||
or not push_config.get("AIBOTK_NAME")
|
||||
if not (
|
||||
(
|
||||
kwargs.get("AIBOTK_KEY")
|
||||
and kwargs.get("AIBOTK_TYPE")
|
||||
and kwargs.get("AIBOTK_NAME")
|
||||
)
|
||||
or (
|
||||
push_config.get("AIBOTK_KEY")
|
||||
and push_config.get("AIBOTK_TYPE")
|
||||
and push_config.get("AIBOTK_NAME")
|
||||
)
|
||||
):
|
||||
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
|
||||
print(
|
||||
"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送"
|
||||
)
|
||||
return
|
||||
print("智能微秘书 服务启动")
|
||||
|
||||
if push_config.get("AIBOTK_TYPE") == "room":
|
||||
if (
|
||||
kwargs.get("AIBOTK_KEY")
|
||||
and kwargs.get("AIBOTK_TYPE")
|
||||
and kwargs.get("AIBOTK_NAME")
|
||||
):
|
||||
AIBOTK_KEY = kwargs.get("AIBOTK_KEY")
|
||||
AIBOTK_TYPE = kwargs.get("AIBOTK_TYPE")
|
||||
AIBOTK_NAME = kwargs.get("AIBOTK_NAME")
|
||||
else:
|
||||
AIBOTK_KEY = push_config.get("AIBOTK_KEY")
|
||||
AIBOTK_TYPE = push_config.get("AIBOTK_TYPE")
|
||||
AIBOTK_NAME = push_config.get("AIBOTK_NAME")
|
||||
if AIBOTK_TYPE == "room":
|
||||
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
|
||||
data = {
|
||||
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||
"roomName": push_config.get("AIBOTK_NAME"),
|
||||
"apiKey": AIBOTK_KEY,
|
||||
"roomName": AIBOTK_NAME,
|
||||
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
|
||||
}
|
||||
else:
|
||||
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
|
||||
data = {
|
||||
"apiKey": push_config.get("AIBOTK_KEY"),
|
||||
"name": push_config.get("AIBOTK_NAME"),
|
||||
"apiKey": AIBOTK_KEY,
|
||||
"name": AIBOTK_NAME,
|
||||
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
|
||||
}
|
||||
body = json.dumps(data).encode(encoding="utf-8")
|
||||
@@ -586,50 +701,75 @@ def aibotk(title: str, content: str) -> None:
|
||||
print(f'智能微秘书 推送失败!{response["error"]}')
|
||||
|
||||
|
||||
def smtp(title: str, content: str) -> None:
|
||||
def smtp(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 SMTP 邮件 推送消息。
|
||||
"""
|
||||
if (
|
||||
not push_config.get("SMTP_SERVER")
|
||||
or not push_config.get("SMTP_SSL")
|
||||
or not push_config.get("SMTP_EMAIL")
|
||||
or not push_config.get("SMTP_PASSWORD")
|
||||
or not push_config.get("SMTP_NAME")
|
||||
if not (
|
||||
(
|
||||
kwargs.get("SMTP_SERVER")
|
||||
and kwargs.get("SMTP_SSL")
|
||||
and kwargs.get("SMTP_EMAIL")
|
||||
and kwargs.get("SMTP_PASSWORD")
|
||||
and kwargs.get("SMTP_NAME")
|
||||
)
|
||||
or (
|
||||
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")
|
||||
)
|
||||
):
|
||||
print(
|
||||
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
|
||||
)
|
||||
return
|
||||
print("SMTP 邮件 服务启动")
|
||||
if (
|
||||
kwargs.get("SMTP_SERVER")
|
||||
and kwargs.get("SMTP_SSL")
|
||||
and kwargs.get("SMTP_EMAIL")
|
||||
and kwargs.get("SMTP_PASSWORD")
|
||||
and kwargs.get("SMTP_NAME")
|
||||
):
|
||||
SMTP_SERVER = kwargs.get("SMTP_SERVER")
|
||||
SMTP_SSL = kwargs.get("SMTP_SSL")
|
||||
SMTP_EMAIL = kwargs.get("SMTP_EMAIL")
|
||||
SMTP_PASSWORD = kwargs.get("SMTP_PASSWORD")
|
||||
SMTP_NAME = kwargs.get("SMTP_NAME")
|
||||
else:
|
||||
SMTP_SERVER = push_config.get("SMTP_SERVER")
|
||||
SMTP_SSL = push_config.get("SMTP_SSL")
|
||||
SMTP_EMAIL = push_config.get("SMTP_EMAIL")
|
||||
SMTP_PASSWORD = push_config.get("SMTP_PASSWORD")
|
||||
SMTP_NAME = push_config.get("SMTP_NAME")
|
||||
|
||||
message = MIMEText(content, "plain", "utf-8")
|
||||
message["From"] = formataddr(
|
||||
(
|
||||
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
Header(SMTP_NAME, "utf-8").encode(),
|
||||
SMTP_EMAIL,
|
||||
)
|
||||
)
|
||||
message["To"] = formataddr(
|
||||
(
|
||||
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
Header(SMTP_NAME, "utf-8").encode(),
|
||||
SMTP_EMAIL,
|
||||
)
|
||||
)
|
||||
message["Subject"] = Header(title, "utf-8")
|
||||
|
||||
try:
|
||||
smtp_server = (
|
||||
smtplib.SMTP_SSL(push_config.get("SMTP_SERVER"))
|
||||
if push_config.get("SMTP_SSL") == "true"
|
||||
else smtplib.SMTP(push_config.get("SMTP_SERVER"))
|
||||
)
|
||||
smtp_server.login(
|
||||
push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD")
|
||||
smtplib.SMTP_SSL(SMTP_SERVER)
|
||||
if SMTP_SSL == "true"
|
||||
else smtplib.SMTP(SMTP_SERVER)
|
||||
)
|
||||
smtp_server.login(SMTP_EMAIL, SMTP_PASSWORD)
|
||||
smtp_server.sendmail(
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
SMTP_EMAIL,
|
||||
SMTP_EMAIL,
|
||||
message.as_bytes(),
|
||||
)
|
||||
smtp_server.close()
|
||||
@@ -638,6 +778,198 @@ def smtp(title: str, content: str) -> None:
|
||||
print(f"SMTP 邮件 推送失败!{e}")
|
||||
|
||||
|
||||
def pushme(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 PushMe 推送消息。
|
||||
"""
|
||||
if not (kwargs.get("PUSHME_KEY") or push_config.get("PUSHME_KEY")):
|
||||
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("PushMe 服务启动")
|
||||
PUSHME_KEY = kwargs.get("PUSHME_KEY", push_config.get("PUSHME_KEY"))
|
||||
|
||||
url = f"https://push.i-i.me/?push_key={PUSHME_KEY}"
|
||||
data = {
|
||||
"title": title,
|
||||
"content": content,
|
||||
}
|
||||
response = requests.post(url, data=data)
|
||||
|
||||
if response.status_code == 200 and response.text == "success":
|
||||
print("PushMe 推送成功!")
|
||||
else:
|
||||
print(f"PushMe 推送失败!{response.status_code} {response.text}")
|
||||
|
||||
|
||||
def chronocat(title: str, content: str, **kwargs) -> None:
|
||||
"""
|
||||
使用 CHRONOCAT 推送消息。
|
||||
"""
|
||||
if not (
|
||||
(
|
||||
push_config.get("CHRONOCAT_URL")
|
||||
and push_config.get("CHRONOCAT_QQ")
|
||||
and push_config.get("CHRONOCAT_TOKEN")
|
||||
)
|
||||
or (
|
||||
push_config.get("CHRONOCAT_URL")
|
||||
and push_config.get("CHRONOCAT_QQ")
|
||||
and push_config.get("CHRONOCAT_TOKEN")
|
||||
)
|
||||
):
|
||||
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
|
||||
return
|
||||
print("CHRONOCAT 服务启动")
|
||||
if (
|
||||
kwargs.get("CHRONOCAT_URL")
|
||||
and kwargs.get("CHRONOCAT_QQ")
|
||||
and kwargs.get("CHRONOCAT_TOKEN")
|
||||
):
|
||||
CHRONOCAT_URL = kwargs.get("CHRONOCAT_URL")
|
||||
CHRONOCAT_QQ = kwargs.get("CHRONOCAT_QQ")
|
||||
CHRONOCAT_TOKEN = kwargs.get("CHRONOCAT_TOKEN")
|
||||
else:
|
||||
CHRONOCAT_URL = push_config.get("CHRONOCAT_URL")
|
||||
CHRONOCAT_QQ = push_config.get("CHRONOCAT_QQ")
|
||||
CHRONOCAT_TOKEN = push_config.get("CHRONOCAT_TOKEN")
|
||||
|
||||
user_ids = re.findall(r"user_id=(\d+)", CHRONOCAT_QQ)
|
||||
group_ids = re.findall(r"group_id=(\d+)", CHRONOCAT_QQ)
|
||||
|
||||
url = f"{CHRONOCAT_URL}/api/message/send"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {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 body
|
||||
|
||||
parsed = parse_string(input_string, value_format_fn)
|
||||
|
||||
if content_type == "application/x-www-form-urlencoded":
|
||||
data = urlencode(parsed, doseq=True)
|
||||
return data
|
||||
|
||||
if content_type == "application/json":
|
||||
data = json.dumps(parsed)
|
||||
return data
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def format_notify_content(url, body, title, content):
|
||||
if "$title" not in url and "$title" not in body:
|
||||
return {}
|
||||
|
||||
formatted_url = url.replace("$title", urllib.parse.quote_plus(title)).replace(
|
||||
"$content", urllib.parse.quote_plus(content)
|
||||
)
|
||||
formatted_body = body.replace("$title", title).replace("$content", content)
|
||||
|
||||
return formatted_url, formatted_body
|
||||
|
||||
|
||||
def custom_notify(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 自定义通知 推送消息。
|
||||
"""
|
||||
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
|
||||
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
|
||||
return
|
||||
|
||||
print("自定义通知服务启动")
|
||||
|
||||
WEBHOOK_URL = push_config.get("WEBHOOK_URL")
|
||||
WEBHOOK_METHOD = push_config.get("WEBHOOK_METHOD")
|
||||
WEBHOOK_CONTENT_TYPE = push_config.get("WEBHOOK_CONTENT_TYPE")
|
||||
WEBHOOK_BODY = push_config.get("WEBHOOK_BODY")
|
||||
WEBHOOK_HEADERS = push_config.get("WEBHOOK_HEADERS")
|
||||
|
||||
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=formatUrl, 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 +980,64 @@ 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():
|
||||
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)
|
||||
if push_config.get("PUSHME_KEY"):
|
||||
notify_function.append(pushme)
|
||||
if (
|
||||
push_config.get("CHRONOCAT_URL")
|
||||
and push_config.get("CHRONOCAT_QQ")
|
||||
and push_config.get("CHRONOCAT_TOKEN")
|
||||
):
|
||||
notify_function.append(chronocat)
|
||||
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
|
||||
notify_function.append(custom_notify)
|
||||
|
||||
|
||||
def send(title: str, content: str) -> None:
|
||||
def send(title: str, content: str, **kwargs) -> None:
|
||||
if not content:
|
||||
print(f"{title} 推送内容为空!")
|
||||
return
|
||||
@@ -707,12 +1050,13 @@ def send(title: str, content: str) -> None:
|
||||
return
|
||||
|
||||
hitokoto = push_config.get("HITOKOTO")
|
||||
content += "\n\n" + one() if hitokoto else ""
|
||||
|
||||
text = one() if hitokoto else ""
|
||||
content += "\n\n" + text
|
||||
|
||||
add_notify_function()
|
||||
ts = [
|
||||
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
|
||||
threading.Thread(
|
||||
target=mode, args=(title, content), kwargs=kwargs, name=mode.__name__
|
||||
)
|
||||
for mode in notify_function
|
||||
]
|
||||
[t.start() for t in ts]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+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
|
||||
+28
-18
@@ -86,10 +86,15 @@ check_server() {
|
||||
fi
|
||||
}
|
||||
|
||||
env_str_to_array() {
|
||||
local IFS="&"
|
||||
read -ra array <<<"${!env_param}"
|
||||
}
|
||||
|
||||
## 正常运行单个脚本,$1:传入参数
|
||||
run_normal() {
|
||||
local file_param=$1
|
||||
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]]; then
|
||||
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]] && [[ "$no_delay" != "true" ]]; then
|
||||
random_delay "$file_param"
|
||||
fi
|
||||
|
||||
@@ -100,7 +105,7 @@ run_normal() {
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
|
||||
$timeoutCmd $which_program $file_param
|
||||
$timeoutCmd $which_program $file_param "${script_params[@]}"
|
||||
}
|
||||
|
||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||
@@ -113,8 +118,7 @@ run_concurrent() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
env_str_to_array
|
||||
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||
local runArr=($(eval echo $tempArr))
|
||||
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
|
||||
@@ -125,11 +129,13 @@ run_concurrent() {
|
||||
let n++
|
||||
done
|
||||
|
||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
local cookieStr=$(
|
||||
IFS="&"
|
||||
echo "${array_run[*]}"
|
||||
)
|
||||
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
env_str_to_array
|
||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
|
||||
|
||||
cd $dir_scripts
|
||||
@@ -139,9 +145,9 @@ run_concurrent() {
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
for i in "${!array[@]}"; do
|
||||
export ${env_param}=${array[i]}
|
||||
export "${env_param}=${array[i]}"
|
||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
eval $timeoutCmd $which_program $file_param &>$single_log_path &
|
||||
eval $timeoutCmd $which_program $file_param "${script_params[@]}" &>$single_log_path &
|
||||
done
|
||||
|
||||
wait
|
||||
@@ -161,8 +167,7 @@ run_designated() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
env_str_to_array
|
||||
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||
local runArr=($(eval echo $tempArr))
|
||||
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
|
||||
@@ -173,8 +178,11 @@ run_designated() {
|
||||
let n++
|
||||
done
|
||||
|
||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
local cookieStr=$(
|
||||
IFS="&"
|
||||
echo "${array_run[*]}"
|
||||
)
|
||||
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
@@ -182,7 +190,7 @@ run_designated() {
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
$timeoutCmd $which_program $file_param
|
||||
$timeoutCmd $which_program $file_param "${script_params[@]}"
|
||||
}
|
||||
|
||||
## 运行其他命令
|
||||
@@ -233,6 +241,8 @@ main() {
|
||||
fi
|
||||
}
|
||||
|
||||
handle_task_before "$@"
|
||||
main "$@"
|
||||
handle_task_after "$@"
|
||||
handle_task_start "${task_shell_params[@]}"
|
||||
run_task_before "${task_shell_params[@]}"
|
||||
main "${task_shell_params[@]}"
|
||||
run_task_after "${task_shell_params[@]}"
|
||||
handle_task_end "${task_shell_params[@]}"
|
||||
|
||||
+56
-88
@@ -2,6 +2,7 @@
|
||||
|
||||
## 目录
|
||||
dir_root=$QL_DIR
|
||||
dir_tmp=$dir_root/.tmp
|
||||
dir_data=$dir_root/data
|
||||
dir_shell=$dir_root/shell
|
||||
dir_sample=$dir_root/sample
|
||||
@@ -18,7 +19,6 @@ dir_update_log=$dir_log/update
|
||||
ql_static_repo=$dir_repo/static
|
||||
|
||||
## 文件
|
||||
file_ecosystem_js=$dir_root/ecosystem.config.js
|
||||
file_config_sample=$dir_sample/config.sample.sh
|
||||
file_env=$dir_config/env.sh
|
||||
file_sharecode=$dir_config/sharecode.sh
|
||||
@@ -68,9 +68,10 @@ import_config() {
|
||||
[[ -f $file_env ]] && . $file_env
|
||||
|
||||
ql_base_url=${QlBaseUrl:-"/"}
|
||||
ql_port=${QlPort:-"5700"}
|
||||
command_timeout_time=${CommandTimeoutTime:-""}
|
||||
proxy_url=${ProxyUrl:-""}
|
||||
file_extensions=${RepoFileExtensions:-"js py"}
|
||||
proxy_url=${ProxyUrl:-""}
|
||||
current_branch=${QL_BRANCH}
|
||||
|
||||
if [[ -n "${DefaultCronRule}" ]]; then
|
||||
@@ -98,9 +99,6 @@ set_proxy() {
|
||||
unset_proxy() {
|
||||
unset http_proxy
|
||||
unset https_proxy
|
||||
unset ftp_proxy
|
||||
unset all_proxy
|
||||
unset no_proxy
|
||||
}
|
||||
|
||||
make_dir() {
|
||||
@@ -175,6 +173,7 @@ define_cmd() {
|
||||
}
|
||||
|
||||
fix_config() {
|
||||
make_dir $dir_tmp
|
||||
make_dir $dir_static
|
||||
make_dir $dir_data
|
||||
make_dir $dir_config
|
||||
@@ -263,6 +262,7 @@ npm_install_sub() {
|
||||
else
|
||||
pnpm install --loglevel error --production
|
||||
fi
|
||||
exit_status=$?
|
||||
}
|
||||
|
||||
npm_install_2() {
|
||||
@@ -283,102 +283,45 @@ diff_and_copy() {
|
||||
fi
|
||||
}
|
||||
|
||||
update_depend() {
|
||||
local dir_current=$(pwd)
|
||||
|
||||
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
|
||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||
npm_install_2 $dir_scripts
|
||||
fi
|
||||
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
git_clone_scripts() {
|
||||
local url="$1"
|
||||
local dir="$2"
|
||||
local branch="$3"
|
||||
local proxy="$4"
|
||||
[[ $branch ]] && local part_cmd="-b $branch "
|
||||
echo -e "开始拉取 $url 到 $dir\n"
|
||||
echo -e "开始拉取仓库 ${uniq_path} 到 $dir\n"
|
||||
|
||||
set_proxy "$proxy"
|
||||
|
||||
git clone --depth=1 $part_cmd $url $dir
|
||||
git clone -q --depth=1 $part_cmd $url $dir
|
||||
exit_status=$?
|
||||
|
||||
reset_branch "$branch" "$dir"
|
||||
|
||||
unset_proxy
|
||||
}
|
||||
|
||||
git_pull_scripts() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work="$1"
|
||||
local branch="$2"
|
||||
local proxy="$3"
|
||||
cd $dir_work
|
||||
echo -e "开始更新仓库:$dir_work"
|
||||
set_proxy "$proxy"
|
||||
|
||||
if [[ ! $branch ]]; then
|
||||
branch=$(cd $dir_work && git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
|
||||
fi
|
||||
|
||||
local pre_commit_id=$(git rev-parse --short HEAD)
|
||||
reset_branch "$branch" "$dir_work"
|
||||
|
||||
git fetch --depth 1 origin $branch
|
||||
exit_status=$?
|
||||
|
||||
reset_branch "$branch" "$dir_work"
|
||||
local cur_commit_id=$(git rev-parse --short HEAD)
|
||||
if [[ $cur_commit_id != $pre_commit_id ]]; then
|
||||
exit_status=0
|
||||
fi
|
||||
|
||||
unset_proxy
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
reset_romote_url() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work=$1
|
||||
local url=$2
|
||||
local branch="$3"
|
||||
|
||||
cd $dir_work
|
||||
if [[ -d "$dir_work/.git" ]]; then
|
||||
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
||||
git remote set-url origin $url &>/dev/null
|
||||
else
|
||||
git init
|
||||
git remote add origin $url &>/dev/null
|
||||
fi
|
||||
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
reset_branch() {
|
||||
local branch="$1"
|
||||
local part_cmd="origin/${branch}"
|
||||
git remote set-branches origin $branch
|
||||
git reset --hard $part_cmd &>/dev/null
|
||||
git checkout -b $branch $part_cmd &>/dev/null
|
||||
}
|
||||
|
||||
random_range() {
|
||||
local beg=$1
|
||||
local end=$2
|
||||
echo $((RANDOM % ($end - $beg) + $beg))
|
||||
}
|
||||
|
||||
delete_pm2() {
|
||||
cd $dir_root
|
||||
pm2 delete ecosystem.config.js
|
||||
}
|
||||
|
||||
reload_pm2() {
|
||||
cd $dir_root
|
||||
# 代理会影响 grpc 服务
|
||||
unset_proxy
|
||||
restore_env_vars
|
||||
pm2 flush &>/dev/null
|
||||
pm2 startOrGracefulReload $file_ecosystem_js
|
||||
pm2 startOrGracefulReload ecosystem.config.js
|
||||
}
|
||||
|
||||
reload_update() {
|
||||
cd $dir_root
|
||||
restore_env_vars
|
||||
pm2 flush &>/dev/null
|
||||
pm2 startOrGracefulReload other.config.js
|
||||
}
|
||||
|
||||
diff_time() {
|
||||
@@ -430,8 +373,6 @@ format_timestamp() {
|
||||
patch_version() {
|
||||
git config --global pull.rebase false
|
||||
|
||||
cp -f $dir_root/.env.example $dir_root/.env
|
||||
|
||||
if [[ -f "$dir_root/db/cookie.db" ]]; then
|
||||
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
||||
mv $dir_root/db/cookie.db $dir_root/db/env.db
|
||||
@@ -471,8 +412,18 @@ init_nginx() {
|
||||
local aliasStr=""
|
||||
local rootStr=""
|
||||
if [[ $ql_base_url != "/" ]]; then
|
||||
if [[ $ql_base_url != /* ]]; then
|
||||
ql_base_url="/$ql_base_url"
|
||||
fi
|
||||
if [[ $ql_base_url != */ ]]; then
|
||||
ql_base_url="$ql_base_url/"
|
||||
fi
|
||||
location_url="^~${ql_base_url%*/}"
|
||||
aliasStr="alias ${dir_static}/dist;"
|
||||
if ! grep -q "<base href=\"$ql_base_url\">" "${dir_static}/dist/index.html"; then
|
||||
awk -v text="<base href=\"$ql_base_url\">" '/<link/ && !inserted {print text; inserted=1} 1' "${dir_static}/dist/index.html" >temp.html
|
||||
mv temp.html "${dir_static}/dist/index.html"
|
||||
fi
|
||||
else
|
||||
rootStr="root ${dir_static}/dist;"
|
||||
fi
|
||||
@@ -481,27 +432,45 @@ init_nginx() {
|
||||
sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf
|
||||
sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf
|
||||
|
||||
ipv6=$(ip a | grep inet6)
|
||||
ipv6Str=""
|
||||
local ipv6=$(ip a | grep inet6)
|
||||
local ipv6Str=""
|
||||
if [[ $ipv6 ]]; then
|
||||
ipv6Str="listen [::]:5700 ipv6only=on;"
|
||||
ipv6Str="listen [::]:${ql_port} ipv6only=on;"
|
||||
fi
|
||||
|
||||
local ipv4Str="listen ${ql_port};"
|
||||
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
|
||||
sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf
|
||||
}
|
||||
|
||||
handle_task_before() {
|
||||
handle_task_start() {
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
|
||||
echo -e "## 开始执行... $begin_time\n"
|
||||
}
|
||||
|
||||
run_task_before() {
|
||||
[[ $is_macos -eq 0 ]] && check_server
|
||||
|
||||
. $file_task_before "$@"
|
||||
|
||||
if [[ $task_before ]]; then
|
||||
echo -e "执行前置命令\n"
|
||||
eval "$task_before"
|
||||
echo -e "\n执行前置命令结束\n"
|
||||
fi
|
||||
}
|
||||
|
||||
handle_task_after() {
|
||||
run_task_after() {
|
||||
. $file_task_after "$@"
|
||||
|
||||
if [[ $task_after ]]; then
|
||||
echo -e "\n执行后置命令\n"
|
||||
eval "$task_after"
|
||||
echo -e "\n执行后置命令结束"
|
||||
fi
|
||||
}
|
||||
|
||||
handle_task_end() {
|
||||
local etime=$(date "+$time_format")
|
||||
local end_time=$(format_time "$time_format" "$etime")
|
||||
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||
@@ -509,8 +478,7 @@ handle_task_after() {
|
||||
|
||||
[[ "$diff_time" == 0 ]] && diff_time=1
|
||||
|
||||
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
||||
|
||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
}
|
||||
|
||||
|
||||
+24
-11
@@ -5,16 +5,16 @@ dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
trap "single_hanle" 2 3 20 15 14
|
||||
trap "single_hanle" 2 3 20 15 14 19 1
|
||||
single_hanle() {
|
||||
eval handle_task_after "$@" "$cmd"
|
||||
eval handle_task_end "$@" "$cmd"
|
||||
exit 1
|
||||
}
|
||||
|
||||
## 选择python3还是node
|
||||
define_program() {
|
||||
local file_param=$1
|
||||
if [[ $file_param == *.js ]]; then
|
||||
if [[ $file_param == *.js ]] || [[ $file_param == *.mjs ]]; then
|
||||
which_program="node"
|
||||
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
|
||||
which_program="python3"
|
||||
@@ -69,11 +69,11 @@ handle_log_path() {
|
||||
if [[ $real_log_path ]]; then
|
||||
log_path="$real_log_path"
|
||||
fi
|
||||
|
||||
cmd=">> $dir_log/$log_path 2>&1"
|
||||
|
||||
cmd="2>&1 | tee -a $dir_log/$log_path"
|
||||
make_dir "$dir_log/$log_dir"
|
||||
if [[ "$show_log" == "true" ]]; then
|
||||
cmd="2>&1 | tee -a $dir_log/$log_path"
|
||||
if [[ "$no_tee" == "true" ]]; then
|
||||
cmd=">> $dir_log/$log_path 2>&1"
|
||||
fi
|
||||
|
||||
if [[ "$real_time" == "true" ]]; then
|
||||
@@ -95,6 +95,21 @@ format_params() {
|
||||
fi
|
||||
fi
|
||||
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
|
||||
|
||||
# 分割 task 内置参数和脚本参数
|
||||
task_shell_params=()
|
||||
script_params=()
|
||||
found_double_dash=false
|
||||
|
||||
for arg in "$@"; do
|
||||
if $found_double_dash; then
|
||||
script_params+=("$arg")
|
||||
elif [ "$arg" == "--" ]; then
|
||||
found_double_dash=true
|
||||
else
|
||||
task_shell_params+=("$arg")
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
init_begin_time() {
|
||||
@@ -119,11 +134,9 @@ if [[ $max_time ]]; then
|
||||
fi
|
||||
|
||||
format_params "$@"
|
||||
define_program "$@"
|
||||
handle_log_path "$@"
|
||||
define_program "${task_shell_params[@]}"
|
||||
handle_log_path "${task_shell_params[@]}"
|
||||
init_begin_time
|
||||
|
||||
eval . $dir_shell/otask.sh "$cmd"
|
||||
[[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
|
||||
|
||||
exit 0
|
||||
|
||||
+103
-53
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/env.sh
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
@@ -31,9 +32,8 @@ output_list_add_drop() {
|
||||
local list=$1
|
||||
local type=$2
|
||||
if [[ -s $list ]]; then
|
||||
echo -e "检测到有${type}的定时任务:\n"
|
||||
echo -e "检测到有${type}的定时任务:"
|
||||
cat $list
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ del_cron() {
|
||||
local path=$2
|
||||
local detail=""
|
||||
local ids=""
|
||||
echo -e "开始尝试自动删除失效的定时任务..."
|
||||
echo -e "\n开始尝试自动删除失效的定时任务..."
|
||||
for cron in $(cat $list_drop); do
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task.* $cron" | perl -pe "s|.*ID=(.*) $cmd_task.* $cron\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
if [[ $ids ]]; then
|
||||
@@ -75,7 +75,7 @@ del_cron() {
|
||||
add_cron() {
|
||||
local list_add=$1
|
||||
local path=$2
|
||||
echo -e "开始尝试自动添加定时任务..."
|
||||
echo -e "\n开始尝试自动添加定时任务..."
|
||||
local detail=""
|
||||
cd $dir_scripts
|
||||
for file in $(cat $list_add); do
|
||||
@@ -85,19 +85,20 @@ add_cron() {
|
||||
cron_line=$(
|
||||
perl -ne "{
|
||||
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
|
||||
}" $file |
|
||||
}" $file 2>/dev/null |
|
||||
perl -pe "{
|
||||
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g;
|
||||
s|\*([\d\*])(.*)|\1\2|g;
|
||||
s| | |g;
|
||||
}" | sort -u | head -1
|
||||
}" 2>/dev/null | sort -u | head -1
|
||||
)
|
||||
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||
[[ -z $cron_name ]] && cron_name="$file_name"
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name:$SUB_ID")
|
||||
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||
[[ -z $cron_name ]] && cron_name=$(grep "name:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||
[[ -z $cron_name ]] && cron_name=$(basename "$file_name")
|
||||
result=$(add_cron_api "${cron_line}:${cmd_task} ${file}:${cron_name}:${SUB_ID}")
|
||||
echo -e "$result"
|
||||
if [[ $detail ]]; then
|
||||
detail="${detail}${result}\n"
|
||||
@@ -134,10 +135,10 @@ update_repo() {
|
||||
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
|
||||
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新${repo_path}成功...\n"
|
||||
echo -e "拉取 ${uniq_path} 成功...\n"
|
||||
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
|
||||
else
|
||||
echo -e "\n更新${repo_path}失败,请检查网络...\n"
|
||||
echo -e "拉取 ${uniq_path} 失败,请检查日志...\n"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -194,7 +195,7 @@ update_raw() {
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name:$SUB_ID")
|
||||
result=$(add_cron_api "${cron_line}:${cmd_task} ${filename}:${cron_name}:${SUB_ID}")
|
||||
echo -e "$result\n"
|
||||
notify_api "新增任务通知" "\n$result"
|
||||
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
||||
@@ -229,58 +230,101 @@ usage() {
|
||||
echo -e "9. $cmd_update resettfa # 禁用两步登录"
|
||||
}
|
||||
|
||||
reload_qinglong() {
|
||||
delete_pm2
|
||||
|
||||
local reload_target="${1}"
|
||||
local primary_branch="master"
|
||||
if [[ "${QL_BRANCH}" == "develop" ]] || [[ "${QL_BRANCH}" == "debian" ]] || [[ "${QL_BRANCH}" == "debian-dev" ]]; then
|
||||
primary_branch="${QL_BRANCH}"
|
||||
fi
|
||||
|
||||
if [[ "$reload_target" == 'system' ]]; then
|
||||
rm -rf ${dir_root}/back ${dir_root}/cli ${dir_root}/docker ${dir_root}/sample ${dir_root}/shell ${dir_root}/src
|
||||
mv -f ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
|
||||
rm -rf $dir_static/*
|
||||
mv -f ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
|
||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||
fi
|
||||
|
||||
if [[ "$reload_target" == 'data' ]]; then
|
||||
rm -rf ${dir_root}/data/*
|
||||
mv -f ${dir_tmp}/data/* ${dir_root}/data/
|
||||
fi
|
||||
|
||||
reload_pm2
|
||||
}
|
||||
|
||||
## 更新qinglong
|
||||
update_qinglong() {
|
||||
rm -rf ${dir_tmp}/*
|
||||
local mirror="gitee"
|
||||
local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
|
||||
local downloadQLUrl="https://gitee.com/whyour/qinglong/repository/archive"
|
||||
local downloadStaticUrl="https://gitee.com/whyour/qinglong-static/repository/archive"
|
||||
local githubStatus=$(curl -s --noproxy "*" -m 2 -IL "https://google.com" | grep 200)
|
||||
if [[ ! -z $githubStatus ]]; then
|
||||
mirror="github"
|
||||
downloadQLUrl="https://github.com/whyour/qinglong/archive/refs/heads"
|
||||
downloadStaticUrl="https://github.com/whyour/qinglong-static/archive/refs/heads"
|
||||
fi
|
||||
echo -e "使用 ${mirror} 源更新...\n"
|
||||
export isFirstStartServer=false
|
||||
|
||||
local primary_branch="master"
|
||||
if [[ "${QL_BRANCH}" == "develop" ]]; then
|
||||
primary_branch="develop"
|
||||
if [[ "${QL_BRANCH}" == "develop" ]] || [[ "${QL_BRANCH}" == "debian" ]] || [[ "${QL_BRANCH}" == "debian-dev" ]]; then
|
||||
primary_branch="${QL_BRANCH}"
|
||||
fi
|
||||
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
|
||||
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
|
||||
git_pull_scripts $dir_root ${primary_branch}
|
||||
|
||||
wget -cqO "${dir_tmp}/ql.zip" "${downloadQLUrl}/${primary_branch}.zip"
|
||||
exit_status=$?
|
||||
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新青龙源文件成功...\n"
|
||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||
update_depend
|
||||
echo -e "更新青龙源文件成功...\n"
|
||||
|
||||
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
||||
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
||||
unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}
|
||||
|
||||
update_qinglong_static "$1" "$primary_branch"
|
||||
update_qinglong_static
|
||||
else
|
||||
echo -e "\n更新青龙源文件失败,请检查网络...\n"
|
||||
echo -e "更新青龙源文件失败,请检查网络...\n"
|
||||
fi
|
||||
}
|
||||
|
||||
update_qinglong_static() {
|
||||
local no_restart="$1"
|
||||
local primary_branch="$2"
|
||||
local url="https://${mirror}.com/whyour/qinglong-static.git"
|
||||
rm -rf ${ql_static_repo} &>/dev/null
|
||||
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
|
||||
wget -cqO "${dir_tmp}/static.zip" "${downloadStaticUrl}/${primary_branch}.zip"
|
||||
exit_status=$?
|
||||
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新青龙静态资源成功...\n"
|
||||
echo -e "更新青龙静态资源成功...\n"
|
||||
unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}
|
||||
|
||||
check_update_dep
|
||||
else
|
||||
echo -e "更新青龙静态资源失败,请检查网络...\n"
|
||||
fi
|
||||
}
|
||||
|
||||
check_update_dep() {
|
||||
echo -e "\n开始检测依赖...\n"
|
||||
if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
|
||||
npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
|
||||
fi
|
||||
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n依赖检测安装成功...\n"
|
||||
echo -e "更新包下载成功..."
|
||||
|
||||
if [[ "$needRestart" == 'true' ]]; then
|
||||
delete_pm2
|
||||
|
||||
rm -rf ${dir_root}/back ${dir_root}/cli ${dir_root}/docker ${dir_root}/sample ${dir_root}/shell ${dir_root}/src
|
||||
mv -f ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
|
||||
rm -rf $dir_static/*
|
||||
mv -f ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
|
||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||
|
||||
rm -rf $dir_static/*
|
||||
cp -rf $ql_static_repo/* $dir_static
|
||||
if [[ $no_restart != "no-restart" ]]; then
|
||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||
echo -e "重启面板中..."
|
||||
sleep 3
|
||||
reload_pm2
|
||||
fi
|
||||
else
|
||||
echo -e "\n更新青龙静态资源失败,请检查网络...\n"
|
||||
echo -e "\n依赖检测安装失败,请检查网络...\n"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -379,9 +423,15 @@ gen_list_repo() {
|
||||
fi
|
||||
|
||||
for file in ${files}; do
|
||||
dirPath=$(dirname "$file")
|
||||
filename=$(basename "$file")
|
||||
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
|
||||
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
|
||||
filePath="${uniq_path}/${filename}"
|
||||
if [[ $dirPath ]] && [[ $dirPath != '.' ]]; then
|
||||
mkdir -p "${dir_scripts}/${uniq_path}/${dirPath}"
|
||||
filePath="${uniq_path}/${dirPath}/${filename}"
|
||||
fi
|
||||
cp -f $file "${dir_scripts}/$filePath"
|
||||
echo "$filePath" >>"$dir_list_tmp/${uniq_path}_scripts.list"
|
||||
# cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
# if [[ $cron_id ]]; then
|
||||
# result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
|
||||
@@ -434,8 +484,13 @@ main() {
|
||||
local log_path="${log_dir}/${log_time}.log"
|
||||
local file_path="$dir_log/$log_path"
|
||||
|
||||
cmd=">> $file_path 2>&1"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
cmd="2>&1 | tee -a $file_path"
|
||||
if [[ "$no_tee" == "true" ]]; then
|
||||
cmd=">> $file_path 2>&1"
|
||||
fi
|
||||
if [[ "$real_time" == "true" ]]; then
|
||||
cmd=""
|
||||
fi
|
||||
|
||||
local time_format="%Y-%m-%d %H:%M:%S"
|
||||
local time=$(date "+$time_format")
|
||||
@@ -448,15 +503,14 @@ main() {
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
fi
|
||||
|
||||
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
|
||||
eval echo -e "请移除 -l 参数" $cmd
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $p1 in
|
||||
update)
|
||||
fix_config
|
||||
eval update_qinglong "$2" $cmd
|
||||
local needRestart=${p2:-"true"}
|
||||
eval update_qinglong $cmd
|
||||
;;
|
||||
reload)
|
||||
eval reload_qinglong "$p2" $cmd
|
||||
;;
|
||||
extra)
|
||||
eval run_extra_shell $cmd
|
||||
@@ -513,10 +567,6 @@ main() {
|
||||
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒 " $cmd
|
||||
fi
|
||||
|
||||
if [[ -f $file_path ]]; then
|
||||
cat $file_path
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+22
@@ -1,4 +1,26 @@
|
||||
const baseUrl = window.__ENV__QlBaseUrl || '/';
|
||||
import { setLocale } from '@umijs/max';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export function rootContainer(container: any) {
|
||||
const locales = {
|
||||
'en': require('./locales/en-US.json'),
|
||||
'zh': require('./locales/zh-CN.json'),
|
||||
};
|
||||
let currentLocale = intl.determineLocale({
|
||||
urlLocaleKey: 'lang',
|
||||
cookieLocaleKey: 'lang',
|
||||
localStorageLocaleKey: 'lang',
|
||||
}).slice(0, 2);
|
||||
|
||||
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
|
||||
currentLocale = 'zh';
|
||||
}
|
||||
|
||||
intl.init({ currentLocale, locales });
|
||||
setLocale(currentLocale === 'zh' ? 'zh-CN' : 'en-US');
|
||||
return container;
|
||||
}
|
||||
|
||||
export function modifyClientRenderOpts(memo: any) {
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
|
||||
@@ -28,7 +29,10 @@ const Copy = ({ text }: { text: string }) => {
|
||||
return (
|
||||
<Link onClick={copyText} style={{ marginLeft: 1 }}>
|
||||
<CopyToClipboard text={text}>
|
||||
<Tooltip key="copy" title={copied ? '复制成功' : '复制'}>
|
||||
<Tooltip
|
||||
key="copy"
|
||||
title={copied ? intl.get('复制成功') : intl.get('复制')}
|
||||
>
|
||||
{copied ? <CheckOutlined /> : <CopyOutlined />}
|
||||
</Tooltip>
|
||||
</CopyToClipboard>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
|
||||
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_lc939gab1iq.js'],
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Service, Options } from 'ahooks/lib/useRequest/src/types';
|
||||
import { Spin, Typography } from 'antd';
|
||||
|
||||
export default function Name<
|
||||
TData extends { data?: { name: string } },
|
||||
TParams,
|
||||
>({
|
||||
service,
|
||||
options,
|
||||
}: {
|
||||
service: Service<TData, [TParams]>;
|
||||
options: Options<TData, [TParams]>;
|
||||
}) {
|
||||
const { loading, data } = useRequest(service, options);
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Typography.Text ellipsis={true}>{data?.data?.name}</Typography.Text>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import { Tag, Input } from 'antd';
|
||||
import { TweenOneGroup } from 'rc-tween-one';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
@@ -101,7 +102,7 @@ const EditableTagGroup = ({
|
||||
onClick={showInput}
|
||||
style={{ borderStyle: 'dashed', cursor: 'pointer' }}
|
||||
>
|
||||
<PlusOutlined /> 新建
|
||||
<PlusOutlined /> {intl.get('新建')}
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
import React, {
|
||||
useRef,
|
||||
useEffect,
|
||||
useContext,
|
||||
createContext,
|
||||
useReducer,
|
||||
useState,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { throttle, isNumber, debounce } from 'lodash';
|
||||
|
||||
const initialState = {
|
||||
// 行高度
|
||||
rowHeight: 0,
|
||||
// 当前的scrollTop
|
||||
curScrollTop: 0,
|
||||
// 总行数
|
||||
totalLen: 0,
|
||||
};
|
||||
|
||||
function reducer(state, action) {
|
||||
const { curScrollTop, totalLen, ifScrollTopClear, scrollTop } = action;
|
||||
|
||||
let stateScrollTop = state.curScrollTop;
|
||||
switch (action.type) {
|
||||
// 改变trs 即 改变渲染的列表trs
|
||||
case 'changeTrs':
|
||||
return {
|
||||
...state,
|
||||
curScrollTop,
|
||||
};
|
||||
// 更改totalLen
|
||||
case 'changeTotalLen':
|
||||
if (totalLen === 0) {
|
||||
stateScrollTop = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
totalLen,
|
||||
curScrollTop: stateScrollTop,
|
||||
};
|
||||
|
||||
case 'reset':
|
||||
return {
|
||||
...state,
|
||||
curScrollTop: ifScrollTopClear ? 0 : scrollTop ?? state.curScrollTop,
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
// ==============全局常量 ================== //
|
||||
const DEFAULT_VID = 'vtable';
|
||||
const vidMap = new Map();
|
||||
let preData = 0;
|
||||
|
||||
// ===============context ============== //
|
||||
const ScrollContext = createContext({
|
||||
dispatch: undefined,
|
||||
renderLen: 1,
|
||||
start: 0,
|
||||
offsetStart: 0,
|
||||
// =============
|
||||
rowHeight: initialState.rowHeight,
|
||||
totalLen: 0,
|
||||
vid: DEFAULT_VID,
|
||||
});
|
||||
|
||||
// =============组件 =================== //
|
||||
|
||||
function VCell(props: any): JSX.Element {
|
||||
const { children, ...restProps } = props;
|
||||
|
||||
return (
|
||||
<td {...restProps}>
|
||||
<div>{children[1]}</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
function VRow(props: any, ref: any): JSX.Element {
|
||||
const { rowHeight } = useContext(ScrollContext);
|
||||
|
||||
const { children, style, ...restProps } = props;
|
||||
|
||||
const trRef = useRef<HTMLTableRowElement>(null);
|
||||
|
||||
return (
|
||||
<tr
|
||||
{...restProps}
|
||||
ref={Object.prototype.hasOwnProperty.call(ref, 'current') ? ref : trRef}
|
||||
style={{
|
||||
...style,
|
||||
height: rowHeight || 'auto',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function VWrapper(props: any): JSX.Element {
|
||||
const { children, ...restProps } = props;
|
||||
|
||||
const { renderLen, start, dispatch, vid, totalLen } =
|
||||
useContext(ScrollContext);
|
||||
|
||||
const contents = useMemo(() => {
|
||||
return children[1];
|
||||
}, [children]);
|
||||
|
||||
const contentsLen = useMemo(() => {
|
||||
return contents?.length ?? 0;
|
||||
}, [contents]);
|
||||
|
||||
useEffect(() => {
|
||||
if (totalLen !== contentsLen) {
|
||||
dispatch({
|
||||
type: 'changeTotalLen',
|
||||
totalLen: contentsLen ?? 0,
|
||||
});
|
||||
}
|
||||
}, [contentsLen, dispatch, vid, totalLen]);
|
||||
|
||||
let tempNode = null;
|
||||
if (Array.isArray(contents) && contents.length) {
|
||||
tempNode = [
|
||||
children[0],
|
||||
contents.slice(start, start + (renderLen ?? 1)).map((item) => {
|
||||
if (Array.isArray(item)) {
|
||||
// 兼容antd v4.3.5 --- rc-table 7.8.1及以下
|
||||
return item[0];
|
||||
}
|
||||
// 处理antd ^v4.4.0 --- rc-table ^7.8.2
|
||||
return item;
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
tempNode = children;
|
||||
}
|
||||
|
||||
return <tbody {...restProps}>{tempNode}</tbody>;
|
||||
}
|
||||
|
||||
function VTable(props: any, otherParams): JSX.Element {
|
||||
const { style, children, ...rest } = props;
|
||||
const { width, ...rest_style } = style;
|
||||
|
||||
const { vid, scrollY, resetScrollTopWhenDataChange, rowHeight, scrollTop } =
|
||||
otherParams ?? {};
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
...initialState,
|
||||
curScrollTop: scrollTop,
|
||||
rowHeight,
|
||||
});
|
||||
|
||||
const wrap_tableRef = useRef<HTMLDivElement>(null);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
|
||||
const ifChangeRef = useRef(false);
|
||||
|
||||
// 数据的总条数
|
||||
const [totalLen, setTotalLen] = useState<number>(
|
||||
children[1]?.props?.data?.length ?? 0,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTotalLen(state.totalLen);
|
||||
}, [state.totalLen]);
|
||||
|
||||
// 组件卸载的清除操作
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
vidMap.delete(vid);
|
||||
};
|
||||
}, [vid]);
|
||||
|
||||
// 数据变更
|
||||
useEffect(() => {
|
||||
ifChangeRef.current = true;
|
||||
// console.log('数据变更')
|
||||
if (isNumber(children[1]?.props?.data?.length)) {
|
||||
dispatch({
|
||||
type: 'changeTotalLen',
|
||||
totalLen: children[1]?.props?.data?.length ?? 0,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [children[1].props.data]);
|
||||
|
||||
// table总高度
|
||||
const tableHeight = useMemo<string | number>(() => {
|
||||
let temp: string | number = 'auto';
|
||||
|
||||
if (rowHeight && totalLen) {
|
||||
temp = rowHeight * totalLen;
|
||||
}
|
||||
return temp;
|
||||
}, [totalLen]);
|
||||
|
||||
// table的scrollY值
|
||||
const [tableScrollY, setTableScrollY] = useState(0);
|
||||
|
||||
// tableScrollY 随scrollY / tableHeight 进行变更
|
||||
useEffect(() => {
|
||||
let temp = 0;
|
||||
|
||||
if (typeof scrollY === 'string') {
|
||||
temp =
|
||||
(wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight ?? 0;
|
||||
} else {
|
||||
temp = scrollY;
|
||||
}
|
||||
|
||||
// if (isNumber(tableHeight) && tableHeight < temp) {
|
||||
// temp = tableHeight;
|
||||
// }
|
||||
|
||||
// 处理tableScrollY <= 0的情况
|
||||
if (temp <= 0) {
|
||||
temp = 0;
|
||||
}
|
||||
|
||||
setTableScrollY(temp);
|
||||
}, [scrollY, tableHeight]);
|
||||
|
||||
// 渲染的条数
|
||||
const renderLen = useMemo<number>(() => {
|
||||
let temp = 1;
|
||||
if (rowHeight && totalLen && tableScrollY) {
|
||||
if (tableScrollY <= 0) {
|
||||
temp = 0;
|
||||
} else {
|
||||
const tempRenderLen = ((tableScrollY / rowHeight) | 0) + 10;
|
||||
// console.log('tempRenderLen', tempRenderLen)
|
||||
// temp = tempRenderLen > totalLen ? totalLen : tempRenderLen;
|
||||
temp = tempRenderLen;
|
||||
}
|
||||
}
|
||||
|
||||
return temp;
|
||||
}, [totalLen, tableScrollY]);
|
||||
|
||||
// 渲染中的第一条
|
||||
let start = rowHeight ? (state.curScrollTop / rowHeight) | 0 : 0;
|
||||
|
||||
start = start < 5 ? 0 : start - 5 + 1;
|
||||
|
||||
// 偏移量
|
||||
let offsetStart = state.curScrollTop % (rowHeight * 5);
|
||||
|
||||
if (start > 0) {
|
||||
offsetStart = offsetStart % rowHeight;
|
||||
}
|
||||
// console.log(offsetStart)
|
||||
// offsetStart= offsetStart%rowHeight
|
||||
// 用来优化向上滚动出现的空白
|
||||
if (state.curScrollTop && state.curScrollTop >= rowHeight * 5) {
|
||||
// start -= 1
|
||||
// if (offsetStart >= rowHeight) {
|
||||
// offsetStart +=
|
||||
// } else {
|
||||
offsetStart += rowHeight * 4;
|
||||
// }
|
||||
} else {
|
||||
start = 0;
|
||||
}
|
||||
|
||||
// console.log(state.curScrollTop, start, offsetStart)
|
||||
|
||||
// 数据变更 操作scrollTop
|
||||
useEffect(() => {
|
||||
const scrollNode = wrap_tableRef.current?.parentNode as HTMLElement;
|
||||
if (ifChangeRef?.current) {
|
||||
// console.log(scrollNode)
|
||||
ifChangeRef.current = false;
|
||||
|
||||
if (resetScrollTopWhenDataChange) {
|
||||
// 重置scrollTop
|
||||
if (scrollNode) {
|
||||
scrollNode.scrollTop = 0;
|
||||
}
|
||||
|
||||
dispatch({ type: 'reset', ifScrollTopClear: true });
|
||||
} else {
|
||||
// console.log(preData)
|
||||
// scrollNode.scrollTop = preData+53
|
||||
// 不重置scrollTop 不清空curScrollTop
|
||||
dispatch({ type: 'reset', ifScrollTopClear: false });
|
||||
}
|
||||
}
|
||||
|
||||
if (vidMap.has(vid)) {
|
||||
vidMap.set(vid, {
|
||||
...vidMap.get(vid),
|
||||
scrollNode,
|
||||
});
|
||||
}
|
||||
}, [totalLen, resetScrollTopWhenDataChange, vid, children]);
|
||||
|
||||
useEffect(() => {
|
||||
const throttleScroll = throttle((e) => {
|
||||
const scrollTop: number = e?.target?.scrollTop ?? 0;
|
||||
// const scrollHeight: number = e?.target?.scrollHeight ?? 0
|
||||
// const clientHeight: number = e?.target?.clientHeight ?? 0
|
||||
if (scrollTop) {
|
||||
preData = scrollTop;
|
||||
}
|
||||
// 到底了 没有滚动条就不会触发reachEnd. 建议设置scrolly高度少点或者数据量多点.
|
||||
// 若renderLen大于totalLen, 置空curScrollTop. => table paddingTop会置空.
|
||||
dispatch({
|
||||
type: 'changeTrs',
|
||||
curScrollTop: renderLen <= totalLen ? scrollTop : 0,
|
||||
});
|
||||
}, 60);
|
||||
|
||||
const ref = wrap_tableRef?.current?.parentNode as HTMLElement;
|
||||
|
||||
if (ref) {
|
||||
ref.addEventListener('scroll', throttleScroll, { passive: true });
|
||||
}
|
||||
|
||||
return () => {
|
||||
ref.removeEventListener('scroll', throttleScroll);
|
||||
};
|
||||
}, [renderLen, totalLen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="virtuallist"
|
||||
ref={wrap_tableRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
height: tableHeight,
|
||||
boxSizing: 'border-box',
|
||||
paddingTop: state.curScrollTop,
|
||||
}}
|
||||
>
|
||||
<ScrollContext.Provider
|
||||
value={{
|
||||
dispatch,
|
||||
start,
|
||||
offsetStart,
|
||||
renderLen,
|
||||
totalLen,
|
||||
vid,
|
||||
rowHeight,
|
||||
}}
|
||||
>
|
||||
<table
|
||||
{...rest}
|
||||
ref={tableRef}
|
||||
style={{
|
||||
...rest_style,
|
||||
width,
|
||||
position: 'relative',
|
||||
transform: `translateY(-${offsetStart}px)`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</ScrollContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ================导出===================
|
||||
export function VList(props: {
|
||||
height: number;
|
||||
// 唯一标识
|
||||
vid?: string;
|
||||
rowHeight: number | string;
|
||||
reset?: boolean;
|
||||
scrollTop?: number | string;
|
||||
}): any {
|
||||
const { vid = DEFAULT_VID, rowHeight, height, reset, scrollTop } = props;
|
||||
|
||||
const resetScrollTopWhenDataChange = reset ?? true;
|
||||
|
||||
if (!vidMap.has(vid)) {
|
||||
vidMap.set(vid, { _id: vid });
|
||||
}
|
||||
|
||||
return {
|
||||
table: (p) =>
|
||||
VTable(p, {
|
||||
vid,
|
||||
scrollY: height,
|
||||
rowHeight,
|
||||
resetScrollTopWhenDataChange,
|
||||
scrollTop,
|
||||
}),
|
||||
body: {
|
||||
wrapper: VWrapper,
|
||||
row: VRow,
|
||||
cell: VCell,
|
||||
},
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user