mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
152
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aab8661c8 | ||
|
|
2500d969c1 | ||
|
|
3c3c376faf | ||
|
|
73f8f3c5fa | ||
|
|
494ccb3c08 | ||
|
|
399728b433 | ||
|
|
52a1de5063 | ||
|
|
18f27a9a69 | ||
|
|
07951964a1 | ||
|
|
a1f888af59 | ||
|
|
f7472b6e74 | ||
|
|
a7baeba755 | ||
|
|
e4f733320d | ||
|
|
55c92dc320 | ||
|
|
50769c43dd | ||
|
|
0587644a6b | ||
|
|
87b934aafe | ||
|
|
7a92e7c6ab | ||
|
|
1d8403c0ec | ||
|
|
ef9e38f167 | ||
|
|
c9bd053fbd | ||
|
|
57939391b9 | ||
|
|
394e96bbf8 | ||
|
|
47c194c1f4 | ||
|
|
7d65d96ebd | ||
|
|
224000b63b | ||
|
|
1c18668bad | ||
|
|
f94582b68d | ||
|
|
eb1c00984c | ||
|
|
1a185f5682 | ||
|
|
b6ea8565ec | ||
|
|
2e94d58758 | ||
|
|
472a3088df | ||
|
|
ec3d61a713 | ||
|
|
95459c33ed | ||
|
|
5459b72f63 | ||
|
|
05db2b1df8 | ||
|
|
c3072e7712 | ||
|
|
3fafe4d24d | ||
|
|
32bccb3f3e | ||
|
|
ac04478d1d | ||
|
|
8a18baa921 | ||
|
|
9a399f8de8 | ||
|
|
da639a1f8f | ||
|
|
425e49675a | ||
|
|
8174762c18 | ||
|
|
710a107e73 | ||
|
|
d871585eee | ||
|
|
729b405b0f | ||
|
|
71a7c1b9d3 | ||
|
|
40a831f3a2 | ||
|
|
1befa1bb8c | ||
|
|
8009634b44 | ||
|
|
03a23f6f31 | ||
|
|
124e01e93a | ||
|
|
2370923a75 | ||
|
|
256e4e7a83 | ||
|
|
c0ec063333 | ||
|
|
576408de01 | ||
|
|
cf94ecfb11 | ||
|
|
118c92d9e5 | ||
|
|
280085668e | ||
|
|
bae4073a64 | ||
|
|
af3e358a6a | ||
|
|
64fcbff715 | ||
|
|
f9f78b4e05 | ||
|
|
fa83761d27 | ||
|
|
8173075b67 | ||
|
|
496918131f | ||
|
|
5d64aae452 | ||
|
|
952ea2859e | ||
|
|
25e8e8198d | ||
|
|
3ab5b0d86b | ||
|
|
6df651aa63 | ||
|
|
924dfd6a6f | ||
|
|
07f43538df | ||
|
|
4fa5fa2014 | ||
|
|
f9b9543a6d | ||
|
|
af97543918 | ||
|
|
a508522872 | ||
|
|
f1ca2134b7 | ||
|
|
a9755655b2 | ||
|
|
6a76e82f26 | ||
|
|
6775f5d123 | ||
|
|
ad6e08525c | ||
|
|
51ef4e7476 | ||
|
|
e5b35273f9 | ||
|
|
647ed3b66c | ||
|
|
4667af4ebe | ||
|
|
3f775a0e6c | ||
|
|
fa79de3f05 | ||
|
|
ffa8b25a66 | ||
|
|
05f8bbd26e | ||
|
|
7d43b14f81 | ||
|
|
cecc5aeb15 | ||
|
|
43d6ac2071 | ||
|
|
678e3e2dc6 | ||
|
|
75f91e1473 | ||
|
|
bdc45c538c | ||
|
|
955c7377d7 | ||
|
|
c71abd8c86 | ||
|
|
ab27a4c908 | ||
|
|
026640a757 | ||
|
|
a1501705c1 | ||
|
|
b321530dcf | ||
|
|
02c6ad8004 | ||
|
|
fff572869e | ||
|
|
b733937691 | ||
|
|
0d9eba4b6e | ||
|
|
20c6a1e8bf | ||
|
|
56bc2f0b1d | ||
|
|
ecc55883f0 | ||
|
|
185fd2ff91 | ||
|
|
3822c37fa0 | ||
|
|
4244502949 | ||
|
|
71dd82f74e | ||
|
|
6ea8d361fd | ||
|
|
3f71f9acdb | ||
|
|
418695c4aa | ||
|
|
4c5223ba11 | ||
|
|
2f70e10be7 | ||
|
|
66706ce150 | ||
|
|
3d719baa54 | ||
|
|
926ad067c1 | ||
|
|
ff98c3a499 | ||
|
|
69fcf45231 | ||
|
|
c3704502bf | ||
|
|
a0613d0f39 | ||
|
|
8c0f46420e | ||
|
|
ea4b474b83 | ||
|
|
53ef0fe296 | ||
|
|
c573186e34 | ||
|
|
c229eca315 | ||
|
|
a41dd74048 | ||
|
|
459f465f3b | ||
|
|
b508e97dc1 | ||
|
|
7414a9d33d | ||
|
|
a48d100b2d | ||
|
|
a3044f9d29 | ||
|
|
19cfc9e351 | ||
|
|
6c61ac5106 | ||
|
|
65f7483688 | ||
|
|
8b042d90f3 | ||
|
|
4e5ad6d5f3 | ||
|
|
8b8eae211b | ||
|
|
f4cb3eacf8 | ||
|
|
f6021c8157 | ||
|
|
73601ca853 | ||
|
|
8218d4ba94 | ||
|
|
d47f835531 | ||
|
|
230a8f61b8 | ||
|
|
af5de8372c |
+8
-11
@@ -1,14 +1,11 @@
|
|||||||
UPDATE_PORT=5300
|
GRPC_PORT=5500
|
||||||
PUBLIC_PORT=5400
|
BACK_PORT=5700
|
||||||
CRON_PORT=5500
|
|
||||||
BACK_PORT=5600
|
|
||||||
PORT=5700
|
|
||||||
|
|
||||||
LOG_LEVEL='debug'
|
LOG_LEVEL='info'
|
||||||
|
|
||||||
SECRET='whyour'
|
JWT_SECRET=
|
||||||
|
JWT_EXPIRES_IN=
|
||||||
|
|
||||||
QINIU_AK=''
|
QINIU_AK=
|
||||||
QINIU_SK=''
|
QINIU_SK=
|
||||||
QINIU_SCOPE=''
|
QINIU_SCOPE=
|
||||||
TEMP=''
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
name: Bug Fixer
|
||||||
|
description: Fix this issue following our error handling pattern.
|
||||||
|
---
|
||||||
+56
-28
@@ -14,27 +14,35 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
to_gitlab:
|
code_gitlab:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: pixta-dev/repository-mirroring-action@v1
|
- uses: Yikun/hub-mirror-action@master
|
||||||
with:
|
with:
|
||||||
target_repo_url: git@gitlab.com:whyour/qinglong.git
|
src: github/whyour
|
||||||
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
dst: gitlab/whyour
|
||||||
|
dst_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||||
|
dst_token: ${{ secrets.GITLAB_TOKEN }}
|
||||||
|
static_list: "qinglong"
|
||||||
|
force_update: true
|
||||||
|
|
||||||
to_gitee:
|
code_gitee:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: pixta-dev/repository-mirroring-action@v1
|
- uses: Yikun/hub-mirror-action@master
|
||||||
with:
|
with:
|
||||||
target_repo_url: git@gitee.com:whyour/qinglong.git
|
src: github/whyour
|
||||||
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
dst: gitee/whyour
|
||||||
|
dst_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||||
|
dst_token: ${{ secrets.GITEE_TOKEN }}
|
||||||
|
static_list: "qinglong"
|
||||||
|
force_update: true
|
||||||
|
|
||||||
build-static:
|
build-static:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -71,23 +79,42 @@ jobs:
|
|||||||
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
||||||
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||||
|
|
||||||
mkdir -p ~/.ssh
|
static_gitlab:
|
||||||
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
|
needs: build-static
|
||||||
chmod 600 ~/.ssh/id_rsa
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: Yikun/hub-mirror-action@master
|
||||||
|
with:
|
||||||
|
src: github/whyour
|
||||||
|
dst: gitlab/whyour
|
||||||
|
dst_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||||
|
dst_token: ${{ secrets.GITLAB_TOKEN }}
|
||||||
|
static_list: "qinglong-static"
|
||||||
|
force_update: true
|
||||||
|
|
||||||
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
|
static_gitee:
|
||||||
git remote add gitee "${REPO_GITEE}"
|
needs: build-static
|
||||||
git remote add gitlab "${REPO_GITLAB}"
|
runs-on: ubuntu-latest
|
||||||
git gc
|
steps:
|
||||||
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
- uses: actions/checkout@v4
|
||||||
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: Yikun/hub-mirror-action@master
|
||||||
|
with:
|
||||||
|
src: github/whyour
|
||||||
|
dst: gitee/whyour
|
||||||
|
dst_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||||
|
dst_token: ${{ secrets.GITEE_TOKEN }}
|
||||||
|
static_list: "qinglong-static"
|
||||||
|
force_update: true
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: build-static
|
needs: build-static
|
||||||
|
|
||||||
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用
|
runs-on: ubuntu-22.04
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
# runs-on: self-hosted
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
@@ -103,7 +130,7 @@ jobs:
|
|||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Setup timezone
|
- name: Setup timezone
|
||||||
uses: szenius/set-timezone@v1.2
|
uses: szenius/set-timezone@v2.0
|
||||||
with:
|
with:
|
||||||
timezoneLinux: Asia/Shanghai
|
timezoneLinux: Asia/Shanghai
|
||||||
|
|
||||||
@@ -149,15 +176,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
build-args: |
|
build-args: |
|
||||||
MAINTAINER=${{ github.repository_owner }}
|
MAINTAINER=${{ github.repository_owner }}
|
||||||
QL_BRANCH=${{ github.ref_name }}
|
QL_BRANCH=${{ github.ref_name }}
|
||||||
SOURCE_COMMIT=${{ github.sha }}
|
SOURCE_COMMIT=${{ github.sha }}
|
||||||
network: host
|
network: host
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
# linux/s390x npm 暂不可用
|
||||||
# platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile
|
file: ./docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
@@ -174,7 +201,7 @@ jobs:
|
|||||||
if: ${{ github.ref_name == 'master' }}
|
if: ${{ github.ref_name == 'master' }}
|
||||||
needs: build-static
|
needs: build-static
|
||||||
|
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
@@ -190,7 +217,7 @@ jobs:
|
|||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Setup timezone
|
- name: Setup timezone
|
||||||
uses: szenius/set-timezone@v1.2
|
uses: szenius/set-timezone@v2.0
|
||||||
with:
|
with:
|
||||||
timezoneLinux: Asia/Shanghai
|
timezoneLinux: Asia/Shanghai
|
||||||
|
|
||||||
@@ -215,14 +242,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push python3.10
|
- name: Build and push python3.10
|
||||||
id: docker_build_310
|
id: docker_build_310
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
build-args: |
|
build-args: |
|
||||||
MAINTAINER=${{ github.repository_owner }}
|
MAINTAINER=${{ github.repository_owner }}
|
||||||
QL_BRANCH=${{ github.ref_name }}
|
QL_BRANCH=${{ github.ref_name }}
|
||||||
SOURCE_COMMIT=${{ github.sha }}
|
SOURCE_COMMIT=${{ github.sha }}
|
||||||
network: host
|
network: host
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
# linux/s390x npm 暂不可用
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/310.Dockerfile
|
file: ./docker/310.Dockerfile
|
||||||
push: true
|
push: true
|
||||||
@@ -26,3 +26,5 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
/shell/preload/env.*
|
/shell/preload/env.*
|
||||||
/shell/preload/notify.*
|
/shell/preload/notify.*
|
||||||
|
/shell/preload/*-notify.json
|
||||||
|
/shell/preload/__ql_notify__.*
|
||||||
|
|||||||
+11
-11
@@ -2,22 +2,22 @@
|
|||||||
**/*.svg
|
**/*.svg
|
||||||
**/*.ejs
|
**/*.ejs
|
||||||
**/*.html
|
**/*.html
|
||||||
.umi
|
/.umi
|
||||||
.umi-production
|
/.umi-production
|
||||||
.umi-test
|
/.umi-test
|
||||||
.history
|
/.history
|
||||||
.tmp
|
/.tmp
|
||||||
node_modules
|
/node_modules
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
yarn.lock
|
yarn.lock
|
||||||
package-lock.json
|
package-lock.json
|
||||||
static
|
/static
|
||||||
data
|
/data
|
||||||
DS_Store
|
DS_Store
|
||||||
src/.umi
|
/src/.umi
|
||||||
src/.umi-production
|
/src/.umi-production
|
||||||
src/.umi-test
|
/src/.umi-test
|
||||||
.env.local
|
.env.local
|
||||||
.env
|
.env
|
||||||
version.ts
|
version.ts
|
||||||
|
|||||||
@@ -16,18 +16,8 @@ export default defineConfig({
|
|||||||
favicons: [`https://qn.whyour.cn/favicon.svg`],
|
favicons: [`https://qn.whyour.cn/favicon.svg`],
|
||||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||||
proxy: {
|
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,
|
|
||||||
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
|
|
||||||
},
|
|
||||||
[`${baseUrl}api`]: {
|
[`${baseUrl}api`]: {
|
||||||
target: 'http://127.0.0.1:5600/',
|
target: 'http://127.0.0.1:5700/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
pathRewrite: { [`^${baseUrl}api`]: '/api' },
|
pathRewrite: { [`^${baseUrl}api`]: '/api' },
|
||||||
@@ -43,19 +33,12 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}) as any,
|
}) as any,
|
||||||
externals: {
|
headScripts: [`./api/env.js`],
|
||||||
react: 'window.React',
|
|
||||||
'react-dom': 'window.ReactDOM',
|
|
||||||
},
|
|
||||||
headScripts: [
|
|
||||||
`./api/env.js`,
|
|
||||||
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
|
|
||||||
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
|
|
||||||
],
|
|
||||||
copy: [
|
copy: [
|
||||||
{
|
{
|
||||||
from: 'node_modules/monaco-editor/min/vs',
|
from: 'node_modules/monaco-editor/min/vs',
|
||||||
to: 'static/dist/monaco-editor/min/vs',
|
to: 'static/dist/monaco-editor/min/vs',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
npmClient: 'pnpm',
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-121
@@ -18,9 +18,9 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
|||||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
|
|
||||||
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
[Demo](http://demo.qinglong.online:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
||||||
|
|
||||||
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
[演示](http://demo.qinglong.online:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||

|

|
||||||
@@ -56,130 +56,15 @@ npm i @whyour/qinglong
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Docker (Recommended)
|
[View Documentation](https://qinglong.online/guide/getting-started/installation-guide)
|
||||||
|
|
||||||
```bash
|
## Built-in API
|
||||||
# curl -sSL get.docker.com | sh
|
|
||||||
docker run -dit \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
|
||||||
-p 5700:5700 \
|
|
||||||
# Deployment paths are not required, e.g. /test.
|
|
||||||
-e QlBaseUrl="/" \
|
|
||||||
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
|
||||||
-e QlPort="5700" \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
--restart unless-stopped \
|
|
||||||
whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker-compose (Recommended)
|
[View Documentation](https://qinglong.online/guide/user-guide/built-in-api)
|
||||||
|
|
||||||
```bash
|
|
||||||
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
|
||||||
mkdir qinglong
|
|
||||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
|
||||||
|
|
||||||
# start
|
|
||||||
docker-compose up -d
|
|
||||||
# stop
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Podman (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# https://podman.io/getting-started/installation
|
|
||||||
podman run -dit \
|
|
||||||
--network bridge \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
|
||||||
-p 5700:5700 \
|
|
||||||
# Deployment paths are not required, e.g. /test.
|
|
||||||
-e QlBaseUrl="/" \
|
|
||||||
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
|
||||||
-e QlPort="5700" \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
docker.io/whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Npm
|
|
||||||
|
|
||||||
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3/pnpm yourself
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Debian/Ubuntu
|
|
||||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g node-pre-gyp pnpm@8.3.1
|
|
||||||
npm install -g @whyour/qinglong
|
|
||||||
qinglong
|
|
||||||
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
|
|
||||||
export QL_DIR=""
|
|
||||||
export QL_DATA_DIR=""
|
|
||||||
# Run again
|
|
||||||
qinglong
|
|
||||||
```
|
|
||||||
|
|
||||||
## Built-in commands
|
## Built-in commands
|
||||||
|
|
||||||
- task
|
[View Documentation](https://qinglong.online/guide/user-guide/basic-explanation)
|
||||||
|
|
||||||
```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 |
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
|||||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
|
|
||||||
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
[Demo](http://demo.qinglong.online:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
||||||
|
|
||||||
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
[演示](http://demo.qinglong.online:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||

|

|
||||||
@@ -58,128 +58,15 @@ npm i @whyour/qinglong
|
|||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
### docker (推荐)
|
[查看文档](https://qinglong.online/guide/getting-started/installation-guide)
|
||||||
|
|
||||||
```bash
|
## 内置 API
|
||||||
# curl -sSL get.docker.com | sh
|
|
||||||
docker run -dit \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
|
||||||
-p 5700:5700 \
|
|
||||||
# 部署路径非必须,比如 /test
|
|
||||||
-e QlBaseUrl="/" \
|
|
||||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
|
||||||
-e QlPort="5700" \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
--restart unless-stopped \
|
|
||||||
whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### docker-compose (推荐)
|
[查看文档](https://qinglong.online/guide/user-guide/built-in-api)
|
||||||
|
|
||||||
```bash
|
|
||||||
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
|
||||||
mkdir qinglong
|
|
||||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
|
||||||
|
|
||||||
# 启动
|
|
||||||
docker-compose up -d
|
|
||||||
# 停止
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
### podman (推荐)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# https://podman.io/getting-started/installation
|
|
||||||
podman run -dit \
|
|
||||||
--network bridge \
|
|
||||||
-v $PWD/ql/data:/ql/data \
|
|
||||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
|
||||||
-p 5700:5700 \
|
|
||||||
# 部署路径非必须,比如 /test
|
|
||||||
-e QlBaseUrl="/" \
|
|
||||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
|
||||||
-e QlPort="5700" \
|
|
||||||
--name qinglong \
|
|
||||||
--hostname qinglong \
|
|
||||||
docker.io/whyour/qinglong:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### npm
|
|
||||||
|
|
||||||
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3/pnpm
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Debian/Ubuntu
|
|
||||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g node-pre-gyp pnpm@8.3.1
|
|
||||||
npm install -g @whyour/qinglong
|
|
||||||
qinglong
|
|
||||||
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
|
|
||||||
export QL_DIR=""
|
|
||||||
export QL_DATA_DIR=""
|
|
||||||
# 再次执行
|
|
||||||
qinglong
|
|
||||||
```
|
|
||||||
|
|
||||||
## 内置命令
|
## 内置命令
|
||||||
|
|
||||||
- task
|
[查看文档](https://qinglong.online/guide/user-guide/basic-explanation)
|
||||||
|
|
||||||
```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 | 任务执行时的文件路径 |
|
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
To report a vulnerability, please open a private vulnerability report at <https://github.com/whyour/qinglong/security>.
|
||||||
|
|
||||||
|
While the discovery of new vulnerabilities is rare, we also recommend always using the latest versions of Qinglong to ensure your application remains as secure as possible.
|
||||||
+2
-2
@@ -1,4 +1,3 @@
|
|||||||
import { getFileContentByName, getLastModifyFilePath } from '../config/util';
|
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
@@ -8,6 +7,7 @@ import { celebrate, Joi } from 'celebrate';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { SAMPLE_FILES } from '../config/const';
|
import { SAMPLE_FILES } from '../config/const';
|
||||||
import ConfigService from '../services/config';
|
import ConfigService from '../services/config';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
@@ -78,7 +78,7 @@ export default (app: Router) => {
|
|||||||
if (name.startsWith('data/scripts/')) {
|
if (name.startsWith('data/scripts/')) {
|
||||||
path = join(config.rootPath, name);
|
path = join(config.rootPath, name);
|
||||||
}
|
}
|
||||||
await fs.writeFile(path, content);
|
await writeFileWithLock(path, content);
|
||||||
res.send({ code: 200, message: '保存成功' });
|
res.send({ code: 200, message: '保存成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
|
|||||||
+5
-31
@@ -4,7 +4,8 @@ import { Logger } from 'winston';
|
|||||||
import CronService from '../services/cron';
|
import CronService from '../services/cron';
|
||||||
import CronViewService from '../services/cronView';
|
import CronViewService from '../services/cronView';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import cron_parser from 'cron-parser';
|
import { commonCronSchema } from '../validation/schedule';
|
||||||
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
@@ -170,27 +171,14 @@ export default (app: Router) => {
|
|||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object(commonCronSchema),
|
||||||
command: Joi.string().required(),
|
|
||||||
schedule: Joi.string().required(),
|
|
||||||
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) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
|
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.create(req.body);
|
const data = await cronService.create(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} else {
|
|
||||||
return res.send({ code: 400, message: 'param schedule error' });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
@@ -331,30 +319,16 @@ export default (app: Router) => {
|
|||||||
'/',
|
'/',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
labels: Joi.array().optional().allow(null),
|
...commonCronSchema,
|
||||||
command: Joi.string().required(),
|
|
||||||
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(),
|
id: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
if (
|
|
||||||
!req.body.schedule ||
|
|
||||||
cron_parser.parseExpression(req.body.schedule).hasNext()
|
|
||||||
) {
|
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.update(req.body);
|
const data = await cronService.update(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} else {
|
|
||||||
return res.send({ code: 400, message: 'param schedule error' });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
@@ -418,7 +392,7 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.import_crontab();
|
const data = await cronService.importCrontab();
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
|
|||||||
+13
-8
@@ -8,7 +8,17 @@ const route = Router();
|
|||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/dependencies', route);
|
app.use('/dependencies', route);
|
||||||
|
|
||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
route.get(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
query:
|
||||||
|
Joi.object({
|
||||||
|
searchValue: Joi.string().optional().allow(''),
|
||||||
|
type: Joi.string().optional().allow(''),
|
||||||
|
status: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
@@ -18,7 +28,8 @@ export default (app: Router) => {
|
|||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/',
|
||||||
@@ -32,7 +43,6 @@ export default (app: Router) => {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.create(req.body);
|
const data = await dependenceService.create(req.body);
|
||||||
@@ -54,7 +64,6 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.update(req.body);
|
const data = await dependenceService.update(req.body);
|
||||||
@@ -71,7 +80,6 @@ export default (app: Router) => {
|
|||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.remove(req.body);
|
const data = await dependenceService.remove(req.body);
|
||||||
@@ -88,7 +96,6 @@ export default (app: Router) => {
|
|||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.remove(req.body, true);
|
const data = await dependenceService.remove(req.body, true);
|
||||||
@@ -107,7 +114,6 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.getDb({ id: req.params.id });
|
const data = await dependenceService.getDb({ id: req.params.id });
|
||||||
@@ -124,7 +130,6 @@ export default (app: Router) => {
|
|||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.reInstall(req.body);
|
const data = await dependenceService.reInstall(req.body);
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import { HealthService } from '../services/health';
|
||||||
|
import Container from 'typedi';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/', route);
|
||||||
|
|
||||||
|
route.get('/health', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const healthService = Container.get(HealthService);
|
||||||
|
const health = await healthService.check();
|
||||||
|
res.status(200).send({
|
||||||
|
code: 200,
|
||||||
|
data: health,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
Logger.error('Health check failed:', err);
|
||||||
|
res.status(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: 'Health check failed',
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -9,6 +9,8 @@ import open from './open';
|
|||||||
import dependence from './dependence';
|
import dependence from './dependence';
|
||||||
import system from './system';
|
import system from './system';
|
||||||
import subscription from './subscription';
|
import subscription from './subscription';
|
||||||
|
import update from './update';
|
||||||
|
import health from './health';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
@@ -22,6 +24,8 @@ export default () => {
|
|||||||
dependence(app);
|
dependence(app);
|
||||||
system(app);
|
system(app);
|
||||||
subscription(app);
|
subscription(app);
|
||||||
|
update(app);
|
||||||
|
health(app);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
+75
-26
@@ -1,11 +1,15 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import { NextFunction, Request, Response, Router } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as fs from 'fs';
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { getFileContentByName, readDirs, rmPath } from '../config/util';
|
import {
|
||||||
import { join } from 'path';
|
getFileContentByName,
|
||||||
import { celebrate, Joi } from 'celebrate';
|
readDirs,
|
||||||
|
removeAnsi,
|
||||||
|
rmPath,
|
||||||
|
} from '../config/util';
|
||||||
|
import LogService from '../services/log';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
const blacklist = ['.tmp'];
|
const blacklist = ['.tmp'];
|
||||||
|
|
||||||
@@ -30,16 +34,19 @@ export default (app: Router) => {
|
|||||||
'/detail',
|
'/detail',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
if (blacklist.includes(req.path)) {
|
const logService = Container.get(LogService);
|
||||||
return res.send({ code: 403, message: '暂无权限' });
|
const finalPath = logService.checkFilePath(
|
||||||
}
|
(req.query.path as string) || '',
|
||||||
const filePath = join(
|
(req.query.file as string) || '',
|
||||||
config.logPath,
|
|
||||||
(req.query.path || '') as string,
|
|
||||||
req.query.file as string,
|
|
||||||
);
|
);
|
||||||
const content = await getFileContentByName(filePath);
|
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
||||||
res.send({ code: 200, data: content });
|
return res.send({
|
||||||
|
code: 403,
|
||||||
|
message: '暂无权限',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const content = await getFileContentByName(finalPath);
|
||||||
|
res.send({ code: 200, data: removeAnsi(content) });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
@@ -50,15 +57,18 @@ export default (app: Router) => {
|
|||||||
'/:file',
|
'/:file',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
if (blacklist.includes(req.path)) {
|
const logService = Container.get(LogService);
|
||||||
return res.send({ code: 403, message: '暂无权限' });
|
const finalPath = logService.checkFilePath(
|
||||||
}
|
(req.query.path as string) || '',
|
||||||
const filePath = join(
|
(req.params.file as string) || '',
|
||||||
config.logPath,
|
|
||||||
(req.query.path || '') as string,
|
|
||||||
req.params.file,
|
|
||||||
);
|
);
|
||||||
const content = await getFileContentByName(filePath);
|
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
||||||
|
return res.send({
|
||||||
|
code: 403,
|
||||||
|
message: '暂无权限',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const content = await getFileContentByName(finalPath);
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -77,17 +87,56 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
let { filename, path, type } = req.body as {
|
let { filename, path } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
path: string;
|
||||||
type: string;
|
|
||||||
};
|
};
|
||||||
const filePath = join(config.logPath, path, filename);
|
const logService = Container.get(LogService);
|
||||||
await rmPath(filePath);
|
const finalPath = logService.checkFilePath(path, filename);
|
||||||
|
if (!finalPath || blacklist.includes(path)) {
|
||||||
|
return res.send({
|
||||||
|
code: 403,
|
||||||
|
message: '暂无权限',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await rmPath(finalPath);
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/download',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
let { filename, path } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
const logService = Container.get(LogService);
|
||||||
|
const filePath = logService.checkFilePath(path, filename);
|
||||||
|
if (!filePath) {
|
||||||
|
return res.send({
|
||||||
|
code: 403,
|
||||||
|
message: '暂无权限',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.download(filePath, filename, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+91
-40
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { fileExist, readDirs, readDir, rmPath, IFile } from '../config/util';
|
||||||
fileExist,
|
|
||||||
getFileContentByName,
|
|
||||||
readDirs,
|
|
||||||
getLastModifyFilePath,
|
|
||||||
readDir,
|
|
||||||
rmPath,
|
|
||||||
} from '../config/util';
|
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
@@ -15,6 +8,7 @@ import { celebrate, Joi } from 'celebrate';
|
|||||||
import path, { join, parse } from 'path';
|
import path, { join, parse } from 'path';
|
||||||
import ScriptService from '../services/script';
|
import ScriptService from '../services/script';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
@@ -30,10 +24,17 @@ const upload = multer({ storage: storage });
|
|||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/scripts', route);
|
app.use('/scripts', route);
|
||||||
|
|
||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
route.get(
|
||||||
|
'/',
|
||||||
|
celebrate({
|
||||||
|
query: Joi.object({
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let result = [];
|
let result: IFile[] = [];
|
||||||
const blacklist = [
|
const blacklist = [
|
||||||
'node_modules',
|
'node_modules',
|
||||||
'.git',
|
'.git',
|
||||||
@@ -43,11 +44,11 @@ export default (app: Router) => {
|
|||||||
'package-lock.json',
|
'package-lock.json',
|
||||||
];
|
];
|
||||||
if (req.query.path) {
|
if (req.query.path) {
|
||||||
const targetPath = path.join(
|
result = await readDir(
|
||||||
config.scriptPath,
|
|
||||||
req.query.path as string,
|
req.query.path as string,
|
||||||
|
config.scriptPath,
|
||||||
|
blacklist,
|
||||||
);
|
);
|
||||||
result = await readDir(targetPath, config.scriptPath, blacklist);
|
|
||||||
} else {
|
} else {
|
||||||
result = await readDirs(
|
result = await readDirs(
|
||||||
config.scriptPath,
|
config.scriptPath,
|
||||||
@@ -74,11 +75,17 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/detail',
|
'/detail',
|
||||||
|
celebrate({
|
||||||
|
query: Joi.object({
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
file: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const content = await scriptService.getFile(
|
const content = await scriptService.getFile(
|
||||||
req.query.path as string,
|
req.query?.path as string || '',
|
||||||
req.query.file as string,
|
req.query.file as string,
|
||||||
);
|
);
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
@@ -90,11 +97,19 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/:file',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
file: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
query: Joi.object({
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const content = await scriptService.getFile(
|
const content = await scriptService.getFile(
|
||||||
req.query.path as string,
|
req.query?.path as string || '',
|
||||||
req.params.file,
|
req.params.file,
|
||||||
);
|
);
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
@@ -107,8 +122,16 @@ export default (app: Router) => {
|
|||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/',
|
||||||
upload.single('file'),
|
upload.single('file'),
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
content: Joi.string().optional().allow(''),
|
||||||
|
originFilename: Joi.string().optional().allow(''),
|
||||||
|
directory: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
let { filename, path, content, originFilename, directory } =
|
let { filename, path, content, originFilename, directory } =
|
||||||
req.body as {
|
req.body as {
|
||||||
@@ -130,8 +153,8 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||||
return res.send({
|
return res.send({
|
||||||
code: 430,
|
code: 403,
|
||||||
message: '文件路径禁止访问',
|
message: '暂无权限',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +186,7 @@ export default (app: Router) => {
|
|||||||
await rmPath(originFilePath);
|
await rmPath(originFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await fs.writeFile(filePath, content);
|
await writeFileWithLock(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -181,15 +204,21 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
let { filename, content, path } = req.body as {
|
let { filename, content, path } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
content: string;
|
content: string;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
const filePath = join(config.scriptPath, path, filename);
|
const scriptService = Container.get(ScriptService);
|
||||||
await fs.writeFile(filePath, content);
|
const filePath = scriptService.checkFilePath(path, filename);
|
||||||
|
if (!filePath) {
|
||||||
|
return res.send({
|
||||||
|
code: 403,
|
||||||
|
message: '暂无权限',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await writeFileWithLock(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -202,19 +231,27 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().allow(''),
|
path: Joi.string().optional().allow(''),
|
||||||
type: Joi.string().optional(),
|
type: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
let { filename, path, type } = req.body as {
|
let { filename, path } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
path: string;
|
||||||
type: string;
|
|
||||||
};
|
};
|
||||||
const filePath = join(config.scriptPath, path, filename);
|
if (!path) {
|
||||||
|
path = '';
|
||||||
|
}
|
||||||
|
const scriptService = Container.get(ScriptService);
|
||||||
|
const filePath = scriptService.checkFilePath(path, filename);
|
||||||
|
if (!filePath) {
|
||||||
|
return res.send({
|
||||||
|
code: 403,
|
||||||
|
message: '暂无权限',
|
||||||
|
});
|
||||||
|
}
|
||||||
await rmPath(filePath);
|
await rmPath(filePath);
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -228,24 +265,30 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
let { filename } = req.body as {
|
let { filename, path } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
|
path: string;
|
||||||
};
|
};
|
||||||
const filePath = join(config.scriptPath, filename);
|
if (!path) {
|
||||||
// const stats = fs.statSync(filePath);
|
path = '';
|
||||||
// res.set({
|
}
|
||||||
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
const scriptService = Container.get(ScriptService);
|
||||||
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件
|
const filePath = scriptService.checkFilePath(path, filename);
|
||||||
// 'Content-Length': stats.size //文件大小
|
if (!filePath) {
|
||||||
// });
|
return res.send({
|
||||||
// fs.createReadStream(filePath).pipe(res);
|
code: 403,
|
||||||
|
message: '暂无权限',
|
||||||
|
});
|
||||||
|
}
|
||||||
return res.download(filePath, filename, (err) => {
|
return res.download(filePath, filename, (err) => {
|
||||||
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -266,9 +309,12 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, content, path } = req.body;
|
let { filename, content, path } = req.body;
|
||||||
|
if (!path) {
|
||||||
|
path = '';
|
||||||
|
}
|
||||||
const { name, ext } = parse(filename);
|
const { name, ext } = parse(filename);
|
||||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||||
await fs.writeFile(filePath, content || '', { encoding: 'utf8' });
|
await writeFileWithLock(filePath, content || '');
|
||||||
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.runScript(filePath);
|
const result = await scriptService.runScript(filePath);
|
||||||
@@ -291,6 +337,9 @@ export default (app: Router) => {
|
|||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
let { filename, path, pid } = req.body;
|
let { filename, path, pid } = req.body;
|
||||||
|
if (!path) {
|
||||||
|
path = '';
|
||||||
|
}
|
||||||
const { name, ext } = parse(filename);
|
const { name, ext } = parse(filename);
|
||||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||||
const logPath = join(config.logPath, path, `${name}.swap`);
|
const logPath = join(config.logPath, path, `${name}.swap`);
|
||||||
@@ -318,12 +367,14 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
let { filename, path, type, newFilename } = req.body as {
|
let { filename, path, newFilename } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
path: string;
|
||||||
type: string;
|
|
||||||
newFilename: string;
|
newFilename: string;
|
||||||
};
|
};
|
||||||
|
if (!path) {
|
||||||
|
path = '';
|
||||||
|
}
|
||||||
const filePath = join(config.scriptPath, path, filename);
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
const newPath = join(config.scriptPath, path, newFilename);
|
const newPath = join(config.scriptPath, path, newFilename);
|
||||||
await fs.rename(filePath, newPath);
|
await fs.rename(filePath, newPath);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default (app: Router) => {
|
|||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const data = await subscriptionService.list(
|
const data = await subscriptionService.list(
|
||||||
req.query.searchValue as string,
|
req.query.searchValue as string,
|
||||||
|
req.query.ids as string,
|
||||||
);
|
);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -212,8 +213,8 @@ export default (app: Router) => {
|
|||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.number().required()),
|
||||||
query: Joi.object({
|
query: Joi.object({
|
||||||
force: Joi.boolean().optional(),
|
force: Joi.boolean().optional(),
|
||||||
t: Joi.number()
|
t: Joi.number(),
|
||||||
})
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
|
|||||||
+101
-9
@@ -33,7 +33,7 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const authInfo = await userService.getUserInfo();
|
const authInfo = await userService.getAuthInfo();
|
||||||
const { version, changeLog, changeLogLink, publishTime } =
|
const { version, changeLog, changeLogLink, publishTime } =
|
||||||
await parseVersion(config.versionFile);
|
await parseVersion(config.versionFile);
|
||||||
|
|
||||||
@@ -273,19 +273,20 @@ export default (app: Router) => {
|
|||||||
{
|
{
|
||||||
onStart: async (cp, startTime) => {
|
onStart: async (cp, startTime) => {
|
||||||
res.setHeader('QL-Task-Pid', `${cp.pid}`);
|
res.setHeader('QL-Task-Pid', `${cp.pid}`);
|
||||||
|
res.setHeader('QL-Task-Log', `${logPath}`);
|
||||||
},
|
},
|
||||||
onEnd: async (cp, endTime, diff) => {
|
onEnd: async (cp, endTime, diff) => {
|
||||||
res.end();
|
res.end();
|
||||||
},
|
},
|
||||||
onError: async (message: string) => {
|
onError: async (message: string) => {
|
||||||
res.write(`\n${message}`);
|
res.write(message);
|
||||||
const absolutePath = await handleLogPath(logPath);
|
const absolutePath = await handleLogPath(logPath);
|
||||||
await fs.appendFile(absolutePath, `\n${message}`);
|
await fs.appendFile(absolutePath, message);
|
||||||
},
|
},
|
||||||
onLog: async (message: string) => {
|
onLog: async (message: string) => {
|
||||||
res.write(`\n${message}`);
|
res.write(message);
|
||||||
const absolutePath = await handleLogPath(logPath);
|
const absolutePath = await handleLogPath(logPath);
|
||||||
await fs.appendFile(absolutePath, `\n${message}`);
|
await fs.appendFile(absolutePath, message);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -316,10 +317,15 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/data/export',
|
'/data/export',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
type: Joi.array().items(Joi.string()).optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
await systemService.exportData(res);
|
await systemService.exportData(res, req.body.type);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
@@ -340,12 +346,98 @@ export default (app: Router) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get('/log', async (req: Request, res: Response, next: NextFunction) => {
|
route.get(
|
||||||
|
'/log',
|
||||||
|
celebrate({
|
||||||
|
query: {
|
||||||
|
startTime: Joi.string().allow('').optional(),
|
||||||
|
endTime: Joi.string().allow('').optional(),
|
||||||
|
t: Joi.string().optional(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
await systemService.getSystemLog(res);
|
await systemService.getSystemLog(
|
||||||
|
res,
|
||||||
|
req.query as {
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/log',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
await systemService.deleteSystemLog();
|
||||||
|
res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/auth/reset',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
retries: Joi.number().optional(),
|
||||||
|
twoFactorActivated: Joi.boolean().optional(),
|
||||||
|
password: Joi.string().optional(),
|
||||||
|
username: Joi.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
await userService.resetAuthInfo(req.body);
|
||||||
|
res.send({ code: 200, message: '更新成功' });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/config/timezone',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
timezone: Joi.string().allow('').allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.updateTimezone(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/config/dependence-clean',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
type: Joi.string().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.cleanDependence(req.body.type);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { NextFunction, Request, Response, Router } from 'express';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import SystemService from '../services/system';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/update', route);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
+3
-3
@@ -8,6 +8,7 @@ import path from 'path';
|
|||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { isDemoEnv } from '../config/util';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
@@ -72,9 +73,8 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
if (process.env.DeployEnv === 'demo') {
|
if (isDemoEnv()) {
|
||||||
return res.send({ code: 450, message: '未知错误' });
|
return res.send({ code: 450, message: '未知错误' });
|
||||||
}
|
}
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
@@ -90,7 +90,7 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const authInfo = await userService.getUserInfo();
|
const authInfo = await userService.getAuthInfo();
|
||||||
res.send({
|
res.send({
|
||||||
code: 200,
|
code: 200,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
+237
-27
@@ -1,32 +1,242 @@
|
|||||||
import 'reflect-metadata'; // We need this in order to use @Decorators
|
import 'reflect-metadata';
|
||||||
import config from './config';
|
import cluster, { type Worker } from 'cluster';
|
||||||
|
import compression from 'compression';
|
||||||
|
import cors from 'cors';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import config from './config';
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
|
import { monitoringMiddleware } from './middlewares/monitoring';
|
||||||
|
import { type GrpcServerService } from './services/grpc';
|
||||||
|
import { type HttpServerService } from './services/http';
|
||||||
|
|
||||||
async function startServer() {
|
interface WorkerMetadata {
|
||||||
const app = express();
|
id: number;
|
||||||
|
pid: number;
|
||||||
await require('./loaders/db').default();
|
serviceType: string;
|
||||||
|
startTime: Date;
|
||||||
await require('./loaders/initFile').default();
|
|
||||||
|
|
||||||
await require('./loaders/sentry').default({ expressApp: app });
|
|
||||||
|
|
||||||
await require('./loaders/app').default({ expressApp: app });
|
|
||||||
|
|
||||||
const server = app
|
|
||||||
.listen(config.port, '0.0.0.0', () => {
|
|
||||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
|
||||||
console.debug(`✌️ 后端服务启动成功!`);
|
|
||||||
process.send?.('ready');
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
Logger.error(err);
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
await require('./loaders/server').default({ server });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startServer();
|
class Application {
|
||||||
|
private app: express.Application;
|
||||||
|
private httpServerService?: HttpServerService;
|
||||||
|
private grpcServerService?: GrpcServerService;
|
||||||
|
private isShuttingDown = false;
|
||||||
|
private workerMetadataMap = new Map<number, WorkerMetadata>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.app = express();
|
||||||
|
// 创建一个全局中间件,删除查询参数中的t
|
||||||
|
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
if (req.query.t) {
|
||||||
|
delete req.query.t;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
try {
|
||||||
|
if (cluster.isPrimary) {
|
||||||
|
await this.initializeDatabase();
|
||||||
|
}
|
||||||
|
if (cluster.isPrimary) {
|
||||||
|
this.startMasterProcess();
|
||||||
|
} else {
|
||||||
|
await this.startWorkerProcess();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('Failed to start application:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startMasterProcess() {
|
||||||
|
this.forkWorker('http');
|
||||||
|
this.forkWorker('grpc');
|
||||||
|
|
||||||
|
cluster.on('exit', (worker, code, signal) => {
|
||||||
|
const metadata = this.workerMetadataMap.get(worker.id);
|
||||||
|
if (metadata) {
|
||||||
|
if (!this.isShuttingDown) {
|
||||||
|
Logger.error(
|
||||||
|
`${metadata.serviceType} worker ${worker.process.pid} died (${signal || code
|
||||||
|
}). Restarting...`,
|
||||||
|
);
|
||||||
|
const newWorker = this.forkWorker(metadata.serviceType);
|
||||||
|
Logger.info(
|
||||||
|
`Restarted ${metadata.serviceType} worker (New PID: ${newWorker.process.pid})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.workerMetadataMap.delete(worker.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupMasterShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private forkWorker(serviceType: string): Worker {
|
||||||
|
const worker = cluster.fork({ SERVICE_TYPE: serviceType });
|
||||||
|
|
||||||
|
this.workerMetadataMap.set(worker.id, {
|
||||||
|
id: worker.id,
|
||||||
|
pid: worker.process.pid!,
|
||||||
|
serviceType,
|
||||||
|
startTime: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeDatabase() {
|
||||||
|
const dbLoader = await import('./loaders/db');
|
||||||
|
await dbLoader.default();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupMiddlewares() {
|
||||||
|
this.app.use(helmet({
|
||||||
|
contentSecurityPolicy: false,
|
||||||
|
}));
|
||||||
|
this.app.use(cors(config.cors));
|
||||||
|
this.app.use(compression());
|
||||||
|
this.app.use(monitoringMiddleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupMasterShutdown() {
|
||||||
|
const shutdown = async () => {
|
||||||
|
if (this.isShuttingDown) return;
|
||||||
|
this.isShuttingDown = true;
|
||||||
|
|
||||||
|
const workers = Object.values(cluster.workers || {});
|
||||||
|
const workerPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
workers.forEach((worker) => {
|
||||||
|
if (worker) {
|
||||||
|
const exitPromise = new Promise<void>((resolve) => {
|
||||||
|
worker.once('exit', () => {
|
||||||
|
Logger.info(`Worker ${worker.process.pid} exited`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
worker.send('shutdown');
|
||||||
|
} catch (error) {
|
||||||
|
Logger.warn(
|
||||||
|
`Failed to send shutdown to worker ${worker.process.pid}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
workerPromises.push(exitPromise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
Promise.all(workerPromises),
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
Logger.warn('Worker shutdown timeout reached');
|
||||||
|
resolve();
|
||||||
|
}, 10000);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('Error during worker shutdown:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startWorkerProcess() {
|
||||||
|
const serviceType = process.env.SERVICE_TYPE;
|
||||||
|
if (!serviceType || !['http', 'grpc'].includes(serviceType)) {
|
||||||
|
Logger.error('Invalid SERVICE_TYPE:', serviceType);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(`✌️ ${serviceType} worker started (PID: ${process.pid})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (serviceType === 'http') {
|
||||||
|
await this.startHttpService();
|
||||||
|
} else {
|
||||||
|
await this.startGrpcService();
|
||||||
|
}
|
||||||
|
|
||||||
|
process.send?.('ready');
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`${serviceType} worker failed:`, error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startHttpService() {
|
||||||
|
this.setupMiddlewares();
|
||||||
|
|
||||||
|
const { HttpServerService } = await import('./services/http');
|
||||||
|
this.httpServerService = Container.get(HttpServerService);
|
||||||
|
|
||||||
|
const appLoader = await import('./loaders/app');
|
||||||
|
await appLoader.default({ app: this.app });
|
||||||
|
|
||||||
|
const server = await this.httpServerService.initialize(
|
||||||
|
this.app,
|
||||||
|
config.port,
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverLoader = await import('./loaders/server');
|
||||||
|
await (serverLoader.default as any)({ server });
|
||||||
|
this.setupWorkerShutdown('http');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startGrpcService() {
|
||||||
|
const { GrpcServerService } = await import('./services/grpc');
|
||||||
|
this.grpcServerService = Container.get(GrpcServerService);
|
||||||
|
|
||||||
|
await this.grpcServerService.initialize();
|
||||||
|
this.setupWorkerShutdown('grpc');
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupWorkerShutdown(serviceType: string) {
|
||||||
|
process.on('message', (msg) => {
|
||||||
|
if (msg === 'shutdown') {
|
||||||
|
this.gracefulShutdown(serviceType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const shutdown = () => this.gracefulShutdown(serviceType);
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async gracefulShutdown(serviceType: string) {
|
||||||
|
if (this.isShuttingDown) return;
|
||||||
|
this.isShuttingDown = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (serviceType === 'http') {
|
||||||
|
await this.httpServerService?.shutdown();
|
||||||
|
} else {
|
||||||
|
await this.grpcServerService?.shutdown();
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[${serviceType}] Error during shutdown:`, error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Application();
|
||||||
|
app.start().catch((error) => {
|
||||||
|
Logger.error('Application failed to start:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|||||||
@@ -23,3 +23,29 @@ export const SAMPLE_FILES = [
|
|||||||
target: 'data/scripts/notify.py',
|
target: 'data/scripts/notify.py',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME;
|
||||||
|
|
||||||
|
export const NotificationModeStringMap = {
|
||||||
|
0: 'gotify',
|
||||||
|
1: 'goCqHttpBot',
|
||||||
|
2: 'serverChan',
|
||||||
|
3: 'pushDeer',
|
||||||
|
4: 'bark',
|
||||||
|
5: 'chat',
|
||||||
|
6: 'telegramBot',
|
||||||
|
7: 'dingtalkBot',
|
||||||
|
8: 'weWorkBot',
|
||||||
|
9: 'weWorkApp',
|
||||||
|
10: 'aibotk',
|
||||||
|
11: 'iGot',
|
||||||
|
12: 'pushPlus',
|
||||||
|
13: 'wePlusBot',
|
||||||
|
14: 'email',
|
||||||
|
15: 'pushMe',
|
||||||
|
16: 'feishu',
|
||||||
|
17: 'webhook',
|
||||||
|
18: 'chronocat',
|
||||||
|
19: 'ntfy',
|
||||||
|
20: 'wxPusherBot',
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { request as undiciRequest, Dispatcher } from 'undici';
|
||||||
|
|
||||||
|
type RequestBaseOptions = {
|
||||||
|
dispatcher?: Dispatcher;
|
||||||
|
json?: Record<string, any>;
|
||||||
|
form?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
} & Omit<Dispatcher.RequestOptions<null>, 'origin' | 'path' | 'method'>;
|
||||||
|
|
||||||
|
type RequestOptionsWithOptions = RequestBaseOptions &
|
||||||
|
Partial<Pick<Dispatcher.RequestOptions, 'method'>>;
|
||||||
|
|
||||||
|
type ResponseTypeMap = {
|
||||||
|
json: Record<string, any>;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponseTypeKey = keyof ResponseTypeMap;
|
||||||
|
|
||||||
|
async function request(
|
||||||
|
url: string,
|
||||||
|
options?: RequestOptionsWithOptions,
|
||||||
|
): Promise<Dispatcher.ResponseData<null>> {
|
||||||
|
const { json, form, body, headers = {}, ...rest } = options || {};
|
||||||
|
const finalHeaders = { ...headers } as Record<string, string>;
|
||||||
|
let finalBody = body;
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
finalHeaders['content-type'] = 'application/json';
|
||||||
|
finalBody = JSON.stringify(json);
|
||||||
|
} else if (form) {
|
||||||
|
finalBody = form;
|
||||||
|
delete finalHeaders['content-type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await undiciRequest(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: finalHeaders,
|
||||||
|
body: finalBody,
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post<T extends ResponseTypeKey = 'json'>(
|
||||||
|
url: string,
|
||||||
|
options?: RequestBaseOptions & { responseType?: T },
|
||||||
|
): Promise<ResponseTypeMap[T]> {
|
||||||
|
const resp = await request(url, { ...options, method: 'POST' });
|
||||||
|
|
||||||
|
const rawText = await resp.body.text();
|
||||||
|
|
||||||
|
if (options?.responseType === 'text') {
|
||||||
|
return rawText as ResponseTypeMap[T];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawText) as ResponseTypeMap[T];
|
||||||
|
} catch {
|
||||||
|
return rawText as ResponseTypeMap[T];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const httpClient = {
|
||||||
|
post,
|
||||||
|
request,
|
||||||
|
};
|
||||||
+55
-15
@@ -2,12 +2,56 @@ import dotenv from 'dotenv';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createRandomString } from './share';
|
import { createRandomString } from './share';
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
port: number;
|
||||||
|
grpcPort: number;
|
||||||
|
nodeEnv: string;
|
||||||
|
isDevelopment: boolean;
|
||||||
|
isProduction: boolean;
|
||||||
|
jwt: {
|
||||||
|
secret: string;
|
||||||
|
expiresIn?: string;
|
||||||
|
};
|
||||||
|
cors: {
|
||||||
|
origin: string[];
|
||||||
|
methods: string[];
|
||||||
|
};
|
||||||
|
logs: {
|
||||||
|
level: string;
|
||||||
|
};
|
||||||
|
api: {
|
||||||
|
prefix: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
port: parseInt(process.env.BACK_PORT || '5700', 10),
|
||||||
|
grpcPort: parseInt(process.env.GRPC_PORT || '5500', 10),
|
||||||
|
nodeEnv: process.env.NODE_ENV || 'development',
|
||||||
|
isDevelopment: process.env.NODE_ENV === 'development',
|
||||||
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
|
logs: {
|
||||||
|
level: process.env.LOG_LEVEL || 'silly',
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
prefix: '/api',
|
||||||
|
},
|
||||||
|
jwt: {
|
||||||
|
secret: process.env.JWT_SECRET || 'whyour-secret',
|
||||||
|
expiresIn: process.env.JWT_EXPIRES_IN,
|
||||||
|
},
|
||||||
|
cors: {
|
||||||
|
origin: process.env.CORS_ORIGIN
|
||||||
|
? process.env.CORS_ORIGIN.split(',')
|
||||||
|
: ['*'],
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
if (!process.env.QL_DIR) {
|
if (!process.env.QL_DIR) {
|
||||||
// 声明QL_DIR环境变量
|
|
||||||
let qlHomePath = path.join(__dirname, '../../');
|
let qlHomePath = path.join(__dirname, '../../');
|
||||||
// 生产环境
|
|
||||||
if (qlHomePath.endsWith('/static/')) {
|
if (qlHomePath.endsWith('/static/')) {
|
||||||
qlHomePath = path.join(qlHomePath, '../');
|
qlHomePath = path.join(qlHomePath, '../');
|
||||||
}
|
}
|
||||||
@@ -31,18 +75,20 @@ const tmpPath = path.join(rootPath, '.tmp/');
|
|||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
const configPath = path.join(dataPath, 'config/');
|
const configPath = path.join(dataPath, 'config/');
|
||||||
const scriptPath = path.join(dataPath, 'scripts/');
|
const scriptPath = path.join(dataPath, 'scripts/');
|
||||||
|
const repoPath = path.join(dataPath, 'repo/');
|
||||||
const bakPath = path.join(dataPath, 'bak/');
|
const bakPath = path.join(dataPath, 'bak/');
|
||||||
const logPath = path.join(dataPath, 'log/');
|
const logPath = path.join(dataPath, 'log/');
|
||||||
const dbPath = path.join(dataPath, 'db/');
|
const dbPath = path.join(dataPath, 'db/');
|
||||||
const uploadPath = path.join(dataPath, 'upload/');
|
const uploadPath = path.join(dataPath, 'upload/');
|
||||||
const sshdPath = path.join(dataPath, 'ssh.d/');
|
const sshdPath = path.join(dataPath, 'ssh.d/');
|
||||||
const systemLogPath = path.join(dataPath, 'syslog/');
|
const systemLogPath = path.join(dataPath, 'syslog/');
|
||||||
|
const dependenceCachePath = path.join(dataPath, 'dep_cache/');
|
||||||
|
|
||||||
const envFile = path.join(preloadPath, 'env.sh');
|
const envFile = path.join(preloadPath, 'env.sh');
|
||||||
const jsEnvFile = path.join(preloadPath, 'env.js');
|
const jsEnvFile = path.join(preloadPath, 'env.js');
|
||||||
const pyEnvFile = path.join(preloadPath, 'env.py');
|
const pyEnvFile = path.join(preloadPath, 'env.py');
|
||||||
const jsNotifyFile = path.join(preloadPath, 'notify.js');
|
const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
|
||||||
const pyNotifyFile = path.join(preloadPath, 'notify.py');
|
const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
|
||||||
const confFile = path.join(configPath, 'config.sh');
|
const confFile = path.join(configPath, 'config.sh');
|
||||||
const crontabFile = path.join(configPath, 'crontab.list');
|
const crontabFile = path.join(configPath, 'crontab.list');
|
||||||
const authConfigFile = path.join(configPath, 'auth.json');
|
const authConfigFile = path.join(configPath, 'auth.json');
|
||||||
@@ -64,17 +110,8 @@ if (envFound.error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
port: parseInt(process.env.BACK_PORT as string, 10),
|
...config,
|
||||||
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
jwt: config.jwt,
|
||||||
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',
|
|
||||||
},
|
|
||||||
api: {
|
|
||||||
prefix: '/api',
|
|
||||||
},
|
|
||||||
rootPath,
|
rootPath,
|
||||||
tmpPath,
|
tmpPath,
|
||||||
dataPath,
|
dataPath,
|
||||||
@@ -100,6 +137,7 @@ export default {
|
|||||||
uploadPath,
|
uploadPath,
|
||||||
configPath,
|
configPath,
|
||||||
scriptPath,
|
scriptPath,
|
||||||
|
repoPath,
|
||||||
samplePath,
|
samplePath,
|
||||||
blackFileList: [
|
blackFileList: [
|
||||||
'auth.json',
|
'auth.json',
|
||||||
@@ -116,6 +154,7 @@ export default {
|
|||||||
bakPath,
|
bakPath,
|
||||||
apiWhiteList: [
|
apiWhiteList: [
|
||||||
'/api/user/login',
|
'/api/user/login',
|
||||||
|
'/api/health',
|
||||||
'/open/auth/token',
|
'/open/auth/token',
|
||||||
'/api/user/two-factor/login',
|
'/api/user/two-factor/login',
|
||||||
'/api/system',
|
'/api/system',
|
||||||
@@ -132,4 +171,5 @@ export default {
|
|||||||
sqliteFile,
|
sqliteFile,
|
||||||
sshdPath,
|
sshdPath,
|
||||||
systemLogPath,
|
systemLogPath,
|
||||||
|
dependenceCachePath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ let pickedEnv: Record<string, string>;
|
|||||||
|
|
||||||
function getPickedEnv() {
|
function getPickedEnv() {
|
||||||
if (pickedEnv) return pickedEnv;
|
if (pickedEnv) return pickedEnv;
|
||||||
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
|
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv', 'QL_DIR']);
|
||||||
if (picked.QlBaseUrl) {
|
if (picked.QlBaseUrl) {
|
||||||
if (!picked.QlBaseUrl.startsWith('/')) {
|
if (!picked.QlBaseUrl.startsWith('/')) {
|
||||||
picked.QlBaseUrl = `/${picked.QlBaseUrl}`
|
picked.QlBaseUrl = `/${picked.QlBaseUrl}`;
|
||||||
}
|
}
|
||||||
if (!picked.QlBaseUrl.endsWith('/')) {
|
if (!picked.QlBaseUrl.endsWith('/')) {
|
||||||
picked.QlBaseUrl = `${picked.QlBaseUrl}/`
|
picked.QlBaseUrl = `${picked.QlBaseUrl}/`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pickedEnv = picked as Record<string, string>;
|
pickedEnv = picked as Record<string, string>;
|
||||||
|
|||||||
+150
-106
@@ -1,15 +1,15 @@
|
|||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import got from 'got';
|
|
||||||
import iconv from 'iconv-lite';
|
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import FormData from 'form-data';
|
import psTreeFun from 'ps-tree';
|
||||||
import psTreeFun from 'pstree.remy';
|
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { load } from 'js-yaml';
|
import { load } from 'js-yaml';
|
||||||
import config from './index';
|
import config from './index';
|
||||||
import { TASK_COMMAND } from './const';
|
import { PYTHON_INSTALL_DIR, TASK_COMMAND } from './const';
|
||||||
import Logger from '../loaders/logger';
|
import Logger from '../loaders/logger';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
import { DependenceTypes } from '../data/dependence';
|
||||||
|
import { FormData } from 'undici';
|
||||||
|
|
||||||
export * from './share';
|
export * from './share';
|
||||||
|
|
||||||
@@ -21,6 +21,10 @@ export async function getFileContentByName(fileName: string) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeAnsi(text: string) {
|
||||||
|
return text.replace(/\x1b\[\d+m/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
export async function getLastModifyFilePath(dir: string) {
|
export async function getLastModifyFilePath(dir: string) {
|
||||||
let filePath = '';
|
let filePath = '';
|
||||||
|
|
||||||
@@ -52,78 +56,6 @@ export function getToken(req: any) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNetIp(req: any) {
|
|
||||||
const ipArray = [
|
|
||||||
...new Set([
|
|
||||||
...(req.headers['x-real-ip'] || '').split(','),
|
|
||||||
...(req.headers['x-forwarded-for'] || '').split(','),
|
|
||||||
req.ip,
|
|
||||||
...req.ips,
|
|
||||||
req.socket.remoteAddress,
|
|
||||||
]),
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
let ip = ipArray[0];
|
|
||||||
|
|
||||||
if (ipArray.length > 1) {
|
|
||||||
for (let i = 0; i < ipArray.length; i++) {
|
|
||||||
const ipNumArray = ipArray[i].split('.');
|
|
||||||
const tmp = ipNumArray[0] + '.' + ipNumArray[1];
|
|
||||||
if (
|
|
||||||
tmp === '192.168' ||
|
|
||||||
(ipNumArray[0] === '172' &&
|
|
||||||
ipNumArray[1] >= 16 &&
|
|
||||||
ipNumArray[1] <= 32) ||
|
|
||||||
tmp === '10.7' ||
|
|
||||||
tmp === '127.0'
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ip = ipArray[i];
|
|
||||||
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 csdnApi = got
|
|
||||||
.get(`https://searchplugin.csdn.net/api/v1/ip/get?ip=${ip}`, {
|
|
||||||
timeout: 10000,
|
|
||||||
retry: 0,
|
|
||||||
})
|
|
||||||
.text();
|
|
||||||
const pconlineApi = got
|
|
||||||
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
|
|
||||||
timeout: 10000,
|
|
||||||
retry: 0,
|
|
||||||
})
|
|
||||||
.buffer();
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
||||||
const ua = userAgent.toLowerCase();
|
const ua = userAgent.toLowerCase();
|
||||||
const testUa = (regexp: RegExp) => regexp.test(ua);
|
const testUa = (regexp: RegExp) => regexp.test(ua);
|
||||||
@@ -145,12 +77,19 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
|||||||
system = 'android'; // android系统
|
system = 'android'; // android系统
|
||||||
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
||||||
system = 'ios'; // ios系统
|
system = 'ios'; // ios系统
|
||||||
|
} else if (testUa(/openharmony/g)) {
|
||||||
|
system = 'openharmony'; // openharmony系统
|
||||||
}
|
}
|
||||||
|
|
||||||
let platform = 'desktop';
|
let platform = 'desktop';
|
||||||
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
||||||
platform = 'desktop';
|
platform = 'desktop';
|
||||||
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
|
} else if (
|
||||||
|
system === 'android' ||
|
||||||
|
system === 'ios' ||
|
||||||
|
system === 'openharmony' ||
|
||||||
|
testUa(/mobile/g)
|
||||||
|
) {
|
||||||
platform = 'mobile';
|
platform = 'mobile';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +107,7 @@ export async function fileExist(file: any) {
|
|||||||
|
|
||||||
export async function createFile(file: string, data: string = '') {
|
export async function createFile(file: string, data: string = '') {
|
||||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||||
await fs.writeFile(file, data);
|
await writeFileWithLock(file, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleLogPath(
|
export async function handleLogPath(
|
||||||
@@ -225,19 +164,19 @@ enum FileType {
|
|||||||
'file',
|
'file',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IFile {
|
export interface IFile {
|
||||||
title: string;
|
title: string;
|
||||||
key: string;
|
key: string;
|
||||||
type: 'directory' | 'file';
|
type: 'directory' | 'file';
|
||||||
parent: string;
|
parent: string;
|
||||||
mtime: number;
|
createTime: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
children?: IFile[];
|
children?: IFile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dirSort(a: IFile, b: IFile): number {
|
export function dirSort(a: IFile, b: IFile): number {
|
||||||
if (a.type === 'file' && b.type === 'file') {
|
if (a.type === 'file' && b.type === 'file') {
|
||||||
return b.mtime - a.mtime;
|
return b.createTime - a.createTime;
|
||||||
} else if (a.type === 'directory' && b.type === 'directory') {
|
} else if (a.type === 'directory' && b.type === 'directory') {
|
||||||
return a.title.localeCompare(b.title);
|
return a.title.localeCompare(b.title);
|
||||||
} else {
|
} else {
|
||||||
@@ -271,7 +210,7 @@ export async function readDirs(
|
|||||||
key,
|
key,
|
||||||
type: 'directory',
|
type: 'directory',
|
||||||
parent: relativePath,
|
parent: relativePath,
|
||||||
mtime: stats.mtime.getTime(),
|
createTime: stats.birthtime.getTime(),
|
||||||
children: children.sort(sort),
|
children: children.sort(sort),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -281,7 +220,7 @@ export async function readDirs(
|
|||||||
key,
|
key,
|
||||||
parent: relativePath,
|
parent: relativePath,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
mtime: stats.mtime.getTime(),
|
createTime: stats.birthtime.getTime(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,23 +232,51 @@ export async function readDir(
|
|||||||
dir: string,
|
dir: string,
|
||||||
baseDir: string = '',
|
baseDir: string = '',
|
||||||
blacklist: string[] = [],
|
blacklist: string[] = [],
|
||||||
) {
|
): Promise<IFile[]> {
|
||||||
const relativePath = path.relative(baseDir, dir);
|
const absoluteDir = path.join(baseDir, dir);
|
||||||
const files = await fs.readdir(dir);
|
const relativePath = path.relative(baseDir, absoluteDir);
|
||||||
const result: any = files
|
|
||||||
.filter((x) => !blacklist.includes(x))
|
try {
|
||||||
.map(async (file: string) => {
|
const files = await fs.readdir(absoluteDir);
|
||||||
const subPath = path.join(dir, file);
|
const result: IFile[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const subPath = path.join(absoluteDir, file);
|
||||||
const stats = await fs.lstat(subPath);
|
const stats = await fs.lstat(subPath);
|
||||||
const key = path.join(relativePath, file);
|
const key = path.join(relativePath, file);
|
||||||
return {
|
|
||||||
|
if (blacklist.includes(file) || stats.isSymbolicLink()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
result.push({
|
||||||
title: file,
|
title: file,
|
||||||
type: stats.isDirectory() ? 'directory' : 'file',
|
type: 'directory',
|
||||||
key,
|
key,
|
||||||
parent: relativePath,
|
parent: relativePath,
|
||||||
};
|
createTime: stats.birthtime.getTime(),
|
||||||
|
children: [],
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
title: file,
|
||||||
|
type: 'file',
|
||||||
|
key,
|
||||||
|
parent: relativePath,
|
||||||
|
size: stats.size,
|
||||||
|
createTime: stats.birthtime.getTime(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function promiseExec(command: string): Promise<string> {
|
export async function promiseExec(command: string): Promise<string> {
|
||||||
@@ -340,9 +307,9 @@ export function parseHeaders(headers: string) {
|
|||||||
if (!headers) return {};
|
if (!headers) return {};
|
||||||
|
|
||||||
const parsed: any = {};
|
const parsed: any = {};
|
||||||
let key;
|
let key: string;
|
||||||
let val;
|
let val: string;
|
||||||
let i;
|
let i: number;
|
||||||
|
|
||||||
headers &&
|
headers &&
|
||||||
headers.split('\n').forEach(function parser(line) {
|
headers.split('\n').forEach(function parser(line) {
|
||||||
@@ -421,11 +388,11 @@ export function parseBody(
|
|||||||
|
|
||||||
export function psTree(pid: number): Promise<number[]> {
|
export function psTree(pid: number): Promise<number[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
psTreeFun(pid, (err: any, pids: number[]) => {
|
psTreeFun(pid, (err: any, children) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
resolve(pids.filter((x) => !isNaN(x)));
|
resolve(children.map((x) => Number(x.PID)).filter((x) => !isNaN(x)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -461,7 +428,7 @@ export async function parseVersion(path: string): Promise<IVersion> {
|
|||||||
return load(await fs.readFile(path, 'utf8')) as IVersion;
|
return load(await fs.readFile(path, 'utf8')) as IVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseContentVersion(content: string): Promise<IVersion> {
|
export function parseContentVersion(content: string): IVersion {
|
||||||
return load(content) as IVersion;
|
return load(content) as IVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,13 +436,18 @@ export async function getUniqPath(
|
|||||||
command: string,
|
command: string,
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
let suffix = '';
|
||||||
if (/^\d+$/.test(id)) {
|
if (/^\d+$/.test(id)) {
|
||||||
id = `_${id}`;
|
suffix = `_${id}`;
|
||||||
} else {
|
}
|
||||||
id = '';
|
|
||||||
|
let items = command.split(/ +/);
|
||||||
|
|
||||||
|
const maxTimeCommandIndex = items.findIndex((x) => x === '-m');
|
||||||
|
if (maxTimeCommandIndex !== -1) {
|
||||||
|
items = items.slice(maxTimeCommandIndex + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = command.split(/ +/);
|
|
||||||
let str = items[0];
|
let str = items[0];
|
||||||
if (items[0] === TASK_COMMAND) {
|
if (items[0] === TASK_COMMAND) {
|
||||||
str = items[1];
|
str = items[1];
|
||||||
@@ -499,7 +471,7 @@ export async function getUniqPath(
|
|||||||
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
|
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${str}${id}`;
|
return `${str}${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeJSONParse(value?: string) {
|
export function safeJSONParse(value?: string) {
|
||||||
@@ -510,7 +482,7 @@ export function safeJSONParse(value?: string) {
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(value);
|
return JSON.parse(value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('[JSON.parse失败]', error);
|
Logger.error('[safeJSONParse失败]', error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,3 +497,75 @@ export async function rmPath(path: string) {
|
|||||||
Logger.error('[rmPath失败]', error);
|
Logger.error('[rmPath失败]', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setSystemTimezone(timezone: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!(await fileExist(`/usr/share/zoneinfo/${timezone}`))) {
|
||||||
|
throw new Error('Invalid timezone');
|
||||||
|
}
|
||||||
|
|
||||||
|
await promiseExec(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`);
|
||||||
|
await promiseExec(`echo "${timezone}" > /etc/timezone`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[setSystemTimezone失败]', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGetCommand(type: DependenceTypes, name: string): string {
|
||||||
|
const baseCommands = {
|
||||||
|
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${name}" | head -1`,
|
||||||
|
[DependenceTypes.python3]: `
|
||||||
|
python3 -c "exec('''
|
||||||
|
name='${name}'
|
||||||
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
print(version(name))
|
||||||
|
except:
|
||||||
|
import importlib.util as u
|
||||||
|
import importlib.metadata as m
|
||||||
|
spec=u.find_spec(name)
|
||||||
|
print(name if spec else '')
|
||||||
|
''')"`,
|
||||||
|
[DependenceTypes.linux]: `apk info -es ${name}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return baseCommands[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInstallCommand(type: DependenceTypes, name: string): string {
|
||||||
|
const baseCommands = {
|
||||||
|
[DependenceTypes.nodejs]: 'pnpm add -g',
|
||||||
|
[DependenceTypes.python3]:
|
||||||
|
'pip3 install --disable-pip-version-check --root-user-action=ignore',
|
||||||
|
[DependenceTypes.linux]: 'apk add --no-check-certificate',
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = baseCommands[type];
|
||||||
|
|
||||||
|
if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR) {
|
||||||
|
command = `${command} --prefix=${PYTHON_INSTALL_DIR}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${command} ${name.trim()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUninstallCommand(
|
||||||
|
type: DependenceTypes,
|
||||||
|
name: string,
|
||||||
|
): string {
|
||||||
|
const baseCommands = {
|
||||||
|
[DependenceTypes.nodejs]: 'pnpm remove -g',
|
||||||
|
[DependenceTypes.python3]:
|
||||||
|
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
||||||
|
[DependenceTypes.linux]: 'apk del',
|
||||||
|
};
|
||||||
|
|
||||||
|
return `${baseCommands[type]} ${name.trim()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDemoEnv() {
|
||||||
|
return process.env.DeployEnv === 'demo';
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,30 +41,12 @@ export enum DependenceTypes {
|
|||||||
'linux',
|
'linux',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InstallDependenceCommandTypes {
|
|
||||||
'pnpm add -g',
|
|
||||||
'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 versionDependenceCommandTypes {
|
||||||
'@',
|
'@',
|
||||||
'==',
|
'==',
|
||||||
'=',
|
'=',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum unInstallDependenceCommandTypes {
|
|
||||||
'pnpm remove -g',
|
|
||||||
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
|
||||||
'apk del',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DependenceInstance
|
export interface DependenceInstance
|
||||||
extends Model<Dependence, Dependence>,
|
extends Model<Dependence, Dependence>,
|
||||||
Dependence {}
|
Dependence {}
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ export class Env {
|
|||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.position = options.position;
|
this.position = options.position;
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.remarks = options.remarks;
|
this.remarks = options.remarks || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-9
@@ -1,5 +1,3 @@
|
|||||||
import { IncomingHttpHeaders } from 'http';
|
|
||||||
|
|
||||||
export enum NotificationMode {
|
export enum NotificationMode {
|
||||||
'gotify' = 'gotify',
|
'gotify' = 'gotify',
|
||||||
'goCqHttpBot' = 'goCqHttpBot',
|
'goCqHttpBot' = 'goCqHttpBot',
|
||||||
@@ -20,6 +18,8 @@ export enum NotificationMode {
|
|||||||
'feishu' = 'feishu',
|
'feishu' = 'feishu',
|
||||||
'webhook' = 'webhook',
|
'webhook' = 'webhook',
|
||||||
'chronocat' = 'Chronocat',
|
'chronocat' = 'Chronocat',
|
||||||
|
'ntfy' = 'ntfy',
|
||||||
|
'wxPusherBot' = 'wxPusherBot',
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class NotificationBaseInfo {
|
abstract class NotificationBaseInfo {
|
||||||
@@ -47,9 +47,8 @@ export class PushDeerNotification extends NotificationBaseInfo {
|
|||||||
public pushDeerUrl = '';
|
public pushDeerUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChatNotification extends NotificationBaseInfo {
|
export class synologyChatNotification extends NotificationBaseInfo {
|
||||||
public chatUrl = '';
|
public synologyChatUrl = '';
|
||||||
public chatToken = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BarkNotification extends NotificationBaseInfo {
|
export class BarkNotification extends NotificationBaseInfo {
|
||||||
@@ -59,7 +58,7 @@ export class BarkNotification extends NotificationBaseInfo {
|
|||||||
public barkGroup = 'qinglong';
|
public barkGroup = 'qinglong';
|
||||||
public barkLevel = 'active';
|
public barkLevel = 'active';
|
||||||
public barkUrl = '';
|
public barkUrl = '';
|
||||||
public barkArchive=""
|
public barkArchive = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||||
@@ -99,6 +98,11 @@ export class IGotNotification extends NotificationBaseInfo {
|
|||||||
export class PushPlusNotification extends NotificationBaseInfo {
|
export class PushPlusNotification extends NotificationBaseInfo {
|
||||||
public pushPlusToken = '';
|
public pushPlusToken = '';
|
||||||
public pushPlusUser = '';
|
public pushPlusUser = '';
|
||||||
|
public pushPlusTemplate = '';
|
||||||
|
public pushplusChannel = '';
|
||||||
|
public pushplusWebhook = '';
|
||||||
|
public pushplusCallbackUrl = '';
|
||||||
|
public pushplusTo = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WePlusBotNotification extends NotificationBaseInfo {
|
export class WePlusBotNotification extends NotificationBaseInfo {
|
||||||
@@ -111,6 +115,7 @@ export class EmailNotification extends NotificationBaseInfo {
|
|||||||
public emailService: string = '';
|
public emailService: string = '';
|
||||||
public emailUser: string = '';
|
public emailUser: string = '';
|
||||||
public emailPass: string = '';
|
public emailPass: string = '';
|
||||||
|
public emailTo: string = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PushMeNotification extends NotificationBaseInfo {
|
export class PushMeNotification extends NotificationBaseInfo {
|
||||||
@@ -139,12 +144,28 @@ export class LarkNotification extends NotificationBaseInfo {
|
|||||||
public larkKey = '';
|
public larkKey = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NtfyNotification extends NotificationBaseInfo {
|
||||||
|
public ntfyUrl = '';
|
||||||
|
public ntfyTopic = '';
|
||||||
|
public ntfyPriority = '';
|
||||||
|
public ntfyToken = '';
|
||||||
|
public ntfyUsername = '';
|
||||||
|
public ntfyPassword = '';
|
||||||
|
public ntfyActions = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WxPusherBotNotification extends NotificationBaseInfo {
|
||||||
|
public wxPusherBotAppToken = '';
|
||||||
|
public wxPusherBotTopicIds = '';
|
||||||
|
public wxPusherBotUids = '';
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotificationInfo
|
export interface NotificationInfo
|
||||||
extends GoCqHttpBotNotification,
|
extends GoCqHttpBotNotification,
|
||||||
GotifyNotification,
|
GotifyNotification,
|
||||||
ServerChanNotification,
|
ServerChanNotification,
|
||||||
PushDeerNotification,
|
PushDeerNotification,
|
||||||
ChatNotification,
|
synologyChatNotification,
|
||||||
BarkNotification,
|
BarkNotification,
|
||||||
TelegramBotNotification,
|
TelegramBotNotification,
|
||||||
DingtalkBotNotification,
|
DingtalkBotNotification,
|
||||||
@@ -158,5 +179,6 @@ export interface NotificationInfo
|
|||||||
PushMeNotification,
|
PushMeNotification,
|
||||||
WebhookNotification,
|
WebhookNotification,
|
||||||
ChronocatNotification,
|
ChronocatNotification,
|
||||||
LarkNotification {}
|
LarkNotification,
|
||||||
|
NtfyNotification,
|
||||||
|
WxPusherBotNotification {}
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ export interface AppToken {
|
|||||||
expiration: number;
|
expiration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system';
|
||||||
|
|
||||||
export interface AppInstance extends Model<App, App>, App {}
|
export interface AppInstance extends Model<App, App>, App {}
|
||||||
export const AppModel = sequelize.define<AppInstance>('App', {
|
export const AppModel = sequelize.define<AppInstance>('App', {
|
||||||
|
|||||||
+23
-2
@@ -27,6 +27,7 @@ export enum AuthDataType {
|
|||||||
'notification' = 'notification',
|
'notification' = 'notification',
|
||||||
'removeLogFrequency' = 'removeLogFrequency',
|
'removeLogFrequency' = 'removeLogFrequency',
|
||||||
'systemConfig' = 'systemConfig',
|
'systemConfig' = 'systemConfig',
|
||||||
|
'authConfig' = 'authConfig',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemConfigInfo {
|
export interface SystemConfigInfo {
|
||||||
@@ -36,6 +37,7 @@ export interface SystemConfigInfo {
|
|||||||
nodeMirror?: string;
|
nodeMirror?: string;
|
||||||
pythonMirror?: string;
|
pythonMirror?: string;
|
||||||
linuxMirror?: string;
|
linuxMirror?: string;
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginLogInfo {
|
export interface LoginLogInfo {
|
||||||
@@ -46,11 +48,30 @@ export interface LoginLogInfo {
|
|||||||
status?: LoginStatus;
|
status?: LoginStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthInfo {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
retries: number;
|
||||||
|
lastlogon: number;
|
||||||
|
lastip: string;
|
||||||
|
lastaddr: string;
|
||||||
|
platform: string;
|
||||||
|
isTwoFactorChecking: boolean;
|
||||||
|
token: string;
|
||||||
|
tokens: Record<string, string>;
|
||||||
|
twoFactorActivated: boolean;
|
||||||
|
twoFactorSecret: string;
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type SystemModelInfo = SystemConfigInfo &
|
export type SystemModelInfo = SystemConfigInfo &
|
||||||
Partial<NotificationInfo> &
|
Partial<NotificationInfo> &
|
||||||
LoginLogInfo;
|
LoginLogInfo &
|
||||||
|
Partial<AuthInfo>;
|
||||||
|
|
||||||
export interface SystemInstance extends Model<SystemInfo, SystemInfo>, SystemInfo { }
|
export interface SystemInstance
|
||||||
|
extends Model<SystemInfo, SystemInfo>,
|
||||||
|
SystemInfo {}
|
||||||
export const SystemModel = sequelize.define<SystemInstance>('Auth', {
|
export const SystemModel = sequelize.define<SystemInstance>('Auth', {
|
||||||
ip: DataTypes.STRING,
|
ip: DataTypes.STRING,
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
|
|||||||
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
declare namespace Express {
|
|
||||||
interface Request {
|
|
||||||
platform: 'desktop' | 'mobile';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'pstree.remy';
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export enum ScheduleType {
|
||||||
|
BOOT = '@boot',
|
||||||
|
ONCE = '@once',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScheduleValidator = (schedule?: string) => boolean;
|
||||||
|
export type CronSchedulerPayload = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
schedule: string;
|
||||||
|
command: string;
|
||||||
|
extra_schedules: Array<{ schedule: string }>;
|
||||||
|
};
|
||||||
+9
-10
@@ -5,25 +5,24 @@ import initData from './initData';
|
|||||||
import { Application } from 'express';
|
import { Application } from 'express';
|
||||||
import linkDeps from './deps';
|
import linkDeps from './deps';
|
||||||
import initTask from './initTask';
|
import initTask from './initTask';
|
||||||
|
import initFile from './initFile';
|
||||||
|
|
||||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
export default async ({ app }: { app: Application }) => {
|
||||||
depInjectorLoader();
|
depInjectorLoader();
|
||||||
Logger.info('✌️ Dependency loaded');
|
Logger.info('✌️ Dependency loaded');
|
||||||
console.log('✌️ Dependency loaded');
|
|
||||||
|
|
||||||
await initData();
|
|
||||||
Logger.info('✌️ Init data loaded');
|
|
||||||
console.log('✌️ Init data loaded');
|
|
||||||
|
|
||||||
await linkDeps();
|
await linkDeps();
|
||||||
Logger.info('✌️ Link deps loaded');
|
Logger.info('✌️ Link deps loaded');
|
||||||
console.log('✌️ Link deps loaded');
|
|
||||||
|
initFile();
|
||||||
|
Logger.info('✌️ Init file loaded');
|
||||||
|
|
||||||
|
await initData();
|
||||||
|
Logger.info('✌️ Init data loaded');
|
||||||
|
|
||||||
initTask();
|
initTask();
|
||||||
Logger.info('✌️ Init task loaded');
|
Logger.info('✌️ Init task loaded');
|
||||||
console.log('✌️ Init task loaded');
|
|
||||||
|
|
||||||
expressLoader({ app: expressApp });
|
expressLoader({ app });
|
||||||
Logger.info('✌️ Express loaded');
|
Logger.info('✌️ Express loaded');
|
||||||
console.log('✌️ Express loaded');
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import Container from 'typedi';
|
||||||
|
import CronService from '../services/cron';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
|
||||||
|
await cronService.bootTask();
|
||||||
|
};
|
||||||
+4
-80
@@ -1,15 +1,11 @@
|
|||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import path from 'path';
|
|
||||||
import DataStore from 'nedb';
|
|
||||||
import { EnvModel } from '../data/env';
|
import { EnvModel } from '../data/env';
|
||||||
import { CrontabModel } from '../data/cron';
|
import { CrontabModel } from '../data/cron';
|
||||||
import { DependenceModel } from '../data/dependence';
|
import { DependenceModel } from '../data/dependence';
|
||||||
import { AppModel } from '../data/open';
|
import { AppModel } from '../data/open';
|
||||||
import { SystemModel } from '../data/system';
|
import { SystemModel } from '../data/system';
|
||||||
import { fileExist } from '../config/util';
|
|
||||||
import { SubscriptionModel } from '../data/subscription';
|
import { SubscriptionModel } from '../data/subscription';
|
||||||
import { CrontabViewModel } from '../data/cronView';
|
import { CrontabViewModel } from '../data/cronView';
|
||||||
import config from '../config';
|
|
||||||
import { sequelize } from '../data';
|
import { sequelize } from '../data';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
@@ -50,7 +46,9 @@ export default async () => {
|
|||||||
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
|
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
try {
|
try {
|
||||||
await sequelize.query('alter table Crontabs add column extra_schedules JSON');
|
await sequelize.query(
|
||||||
|
'alter table Crontabs add column extra_schedules JSON',
|
||||||
|
);
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
try {
|
try {
|
||||||
await sequelize.query('alter table Crontabs add column task_before TEXT');
|
await sequelize.query('alter table Crontabs add column task_before TEXT');
|
||||||
@@ -59,82 +57,8 @@ export default async () => {
|
|||||||
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
// 2.10-2.11 升级
|
|
||||||
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
|
||||||
const envDbFile = path.join(config.rootPath, 'db/env.db');
|
|
||||||
const appDbFile = path.join(config.rootPath, 'db/app.db');
|
|
||||||
const authDbFile = path.join(config.rootPath, 'db/auth.db');
|
|
||||||
const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
|
|
||||||
const crondbExist = await fileExist(cronDbFile);
|
|
||||||
const dependenceDbExist = await fileExist(dependenceDbFile);
|
|
||||||
const envDbExist = await fileExist(envDbFile);
|
|
||||||
const appDbExist = await fileExist(appDbFile);
|
|
||||||
const authDbExist = await fileExist(authDbFile);
|
|
||||||
|
|
||||||
const cronCount = await CrontabModel.count();
|
|
||||||
const dependenceCount = await DependenceModel.count();
|
|
||||||
const envCount = await EnvModel.count();
|
|
||||||
const appCount = await AppModel.count();
|
|
||||||
const authCount = await SystemModel.count();
|
|
||||||
if (crondbExist && cronCount === 0) {
|
|
||||||
const cronDb = new DataStore({
|
|
||||||
filename: cronDbFile,
|
|
||||||
autoload: true,
|
|
||||||
});
|
|
||||||
cronDb.persistence.compactDatafile();
|
|
||||||
cronDb.find({}).exec(async (err, docs) => {
|
|
||||||
await CrontabModel.bulkCreate(docs, { ignoreDuplicates: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dependenceDbExist && dependenceCount === 0) {
|
|
||||||
const dependenceDb = new DataStore({
|
|
||||||
filename: dependenceDbFile,
|
|
||||||
autoload: true,
|
|
||||||
});
|
|
||||||
dependenceDb.persistence.compactDatafile();
|
|
||||||
dependenceDb.find({}).exec(async (err, docs) => {
|
|
||||||
await DependenceModel.bulkCreate(docs, { ignoreDuplicates: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (envDbExist && envCount === 0) {
|
|
||||||
const envDb = new DataStore({
|
|
||||||
filename: envDbFile,
|
|
||||||
autoload: true,
|
|
||||||
});
|
|
||||||
envDb.persistence.compactDatafile();
|
|
||||||
envDb.find({}).exec(async (err, docs) => {
|
|
||||||
await EnvModel.bulkCreate(docs, { ignoreDuplicates: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appDbExist && appCount === 0) {
|
|
||||||
const appDb = new DataStore({
|
|
||||||
filename: appDbFile,
|
|
||||||
autoload: true,
|
|
||||||
});
|
|
||||||
appDb.persistence.compactDatafile();
|
|
||||||
appDb.find({}).exec(async (err, docs) => {
|
|
||||||
await AppModel.bulkCreate(docs, { ignoreDuplicates: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authDbExist && authCount === 0) {
|
|
||||||
const authDb = new DataStore({
|
|
||||||
filename: authDbFile,
|
|
||||||
autoload: true,
|
|
||||||
});
|
|
||||||
authDb.persistence.compactDatafile();
|
|
||||||
authDb.find({}).exec(async (err, docs) => {
|
|
||||||
await SystemModel.bulkCreate(docs, { ignoreDuplicates: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✌️ DB loaded');
|
|
||||||
Logger.info('✌️ DB loaded');
|
Logger.info('✌️ DB loaded');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('✌️ DB load failed');
|
Logger.error('✌️ DB load failed', error);
|
||||||
Logger.error(error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ async function linkCommand() {
|
|||||||
const source = path.join(config.rootPath, 'shell', link.src);
|
const source = path.join(config.rootPath, 'shell', link.src);
|
||||||
const target = path.join(commandDir, link.dest);
|
const target = path.join(commandDir, link.dest);
|
||||||
const tmpTarget = path.join(commandDir, link.tmp);
|
const tmpTarget = path.join(commandDir, link.tmp);
|
||||||
|
try {
|
||||||
|
const stats = await fs.lstat(tmpTarget);
|
||||||
|
if (stats) {
|
||||||
|
await fs.unlink(tmpTarget);
|
||||||
|
}
|
||||||
|
} catch (error) { }
|
||||||
await fs.symlink(source, tmpTarget);
|
await fs.symlink(source, tmpTarget);
|
||||||
await fs.rename(tmpTarget, target);
|
await fs.rename(tmpTarget, target);
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-36
@@ -3,19 +3,13 @@ import bodyParser from 'body-parser';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import routes from '../api';
|
import routes from '../api';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import jwt, { UnauthorizedError } from 'express-jwt';
|
import { UnauthorizedError, expressjwt } from 'express-jwt';
|
||||||
import fs from 'fs/promises';
|
import { getPlatform, getToken } from '../config/util';
|
||||||
import { getPlatform, getToken, safeJSONParse } from '../config/util';
|
|
||||||
import Container from 'typedi';
|
|
||||||
import OpenService from '../services/open';
|
|
||||||
import rewrite from 'express-urlrewrite';
|
import rewrite from 'express-urlrewrite';
|
||||||
import UserService from '../services/user';
|
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { EnvModel } from '../data/env';
|
|
||||||
import { errors } from 'celebrate';
|
import { errors } from 'celebrate';
|
||||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
||||||
import { serveEnv } from '../config/serverEnv';
|
import { serveEnv } from '../config/serverEnv';
|
||||||
import Logger from './logger';
|
import { IKeyvStore, shareStore } from '../shared/store';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
export default ({ app }: { app: Application }) => {
|
export default ({ app }: { app: Application }) => {
|
||||||
app.set('trust proxy', 'loopback');
|
app.set('trust proxy', 'loopback');
|
||||||
@@ -23,25 +17,18 @@ export default ({ app }: { app: Application }) => {
|
|||||||
app.get(`${config.api.prefix}/env.js`, serveEnv);
|
app.get(`${config.api.prefix}/env.js`, serveEnv);
|
||||||
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
||||||
|
|
||||||
app.use(
|
|
||||||
'/api/public',
|
|
||||||
createProxyMiddleware({
|
|
||||||
target: `http://0.0.0.0:${config.publicPort}/api`,
|
|
||||||
changeOrigin: true,
|
|
||||||
pathRewrite: { '/api/public': '' },
|
|
||||||
logProvider: () => Logger,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
|
|
||||||
|
const frontendPath = path.join(config.rootPath, 'static/dist');
|
||||||
|
app.use(express.static(frontendPath));
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
jwt({
|
expressjwt({
|
||||||
secret: config.secret,
|
secret: config.jwt.secret,
|
||||||
algorithms: ['HS384'],
|
algorithms: ['HS384'],
|
||||||
}).unless({
|
}).unless({
|
||||||
path: [...config.apiWhiteList, /^\/open\//],
|
path: [...config.apiWhiteList, /^\/(?!api\/).*/],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,11 +42,17 @@ export default ({ app }: { app: Application }) => {
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(async (req, res, next) => {
|
app.use(async (req: Request, res, next) => {
|
||||||
|
if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
const headerToken = getToken(req);
|
const headerToken = getToken(req);
|
||||||
if (req.path.startsWith('/open/')) {
|
if (req.path.startsWith('/open/')) {
|
||||||
const openService = Container.get(OpenService);
|
const apps = await shareStore.getApps();
|
||||||
const doc = await openService.findTokenByValue(headerToken);
|
const doc = apps?.filter((x) =>
|
||||||
|
x.tokens?.find((y) => y.value === headerToken),
|
||||||
|
)?.[0];
|
||||||
if (doc && doc.tokens && doc.tokens.length > 0) {
|
if (doc && doc.tokens && doc.tokens.length > 0) {
|
||||||
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
||||||
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
||||||
@@ -83,9 +76,9 @@ export default ({ app }: { app: Application }) => {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fs.readFile(config.authConfigFile, 'utf8');
|
const authInfo = await shareStore.getAuthInfo();
|
||||||
if (data && headerToken) {
|
if (authInfo && headerToken) {
|
||||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
const { token = '', tokens = {} } = authInfo;
|
||||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
@@ -103,8 +96,8 @@ export default ({ app }: { app: Application }) => {
|
|||||||
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const userService = Container.get(UserService);
|
const authInfo =
|
||||||
const authInfo = await userService.getUserInfo();
|
(await shareStore.getAuthInfo()) || ({} as IKeyvStore['authInfo']);
|
||||||
|
|
||||||
let isInitialized = true;
|
let isInitialized = true;
|
||||||
if (
|
if (
|
||||||
@@ -125,10 +118,15 @@ export default ({ app }: { app: Application }) => {
|
|||||||
app.use(rewrite('/open/*', '/api/$1'));
|
app.use(rewrite('/open/*', '/api/$1'));
|
||||||
app.use(config.api.prefix, routes());
|
app.use(config.api.prefix, routes());
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.get('*', (_, res, next) => {
|
||||||
|
const indexPath = path.join(frontendPath, 'index.html');
|
||||||
|
res.sendFile(indexPath, (err) => {
|
||||||
|
if (err) {
|
||||||
const err: any = new Error('Not Found');
|
const err: any = new Error('Not Found');
|
||||||
err['status'] = 404;
|
err['status'] = 404;
|
||||||
next(err);
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(errors());
|
app.use(errors());
|
||||||
@@ -162,8 +160,8 @@ export default ({ app }: { app: Application }) => {
|
|||||||
.status(500)
|
.status(500)
|
||||||
.send({
|
.send({
|
||||||
code: 400,
|
code: 400,
|
||||||
message: `${err.name} ${err.message}`,
|
message: `${err.message}`,
|
||||||
validation: err.errors,
|
errors: err.errors,
|
||||||
})
|
})
|
||||||
.end();
|
.end();
|
||||||
}
|
}
|
||||||
@@ -178,8 +176,6 @@ export default ({ app }: { app: Application }) => {
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
) => {
|
) => {
|
||||||
Sentry.captureException(err);
|
|
||||||
|
|
||||||
res.status(err.status || 500);
|
res.status(err.status || 500);
|
||||||
res.json({
|
res.json({
|
||||||
code: err.status || 500,
|
code: err.status || 500,
|
||||||
|
|||||||
+68
-10
@@ -11,39 +11,90 @@ import { CrontabViewModel, CronViewType } from '../data/cronView';
|
|||||||
import { initPosition } from '../data/env';
|
import { initPosition } from '../data/env';
|
||||||
import { AuthDataType, SystemModel } from '../data/system';
|
import { AuthDataType, SystemModel } from '../data/system';
|
||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
|
import UserService from '../services/user';
|
||||||
|
import { writeFile, readFile } from 'fs/promises';
|
||||||
|
import { createRandomString, fileExist, safeJSONParse } from '../config/util';
|
||||||
|
import OpenService from '../services/open';
|
||||||
|
import { shareStore } from '../shared/store';
|
||||||
|
import Logger from './logger';
|
||||||
|
import { AppModel } from '../data/open';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
|
||||||
// 初始化增加系统配置
|
// 初始化增加系统配置
|
||||||
await SystemModel.upsert({ type: AuthDataType.systemConfig });
|
let systemApp = (
|
||||||
await SystemModel.upsert({ type: AuthDataType.notification });
|
await AppModel.findOne({
|
||||||
|
where: { name: 'system' },
|
||||||
|
})
|
||||||
|
)?.get({ plain: true });
|
||||||
|
if (!systemApp) {
|
||||||
|
systemApp = await AppModel.create({
|
||||||
|
name: 'system',
|
||||||
|
scopes: ['crons', 'system'],
|
||||||
|
client_id: createRandomString(12, 12),
|
||||||
|
client_secret: createRandomString(24, 24),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const [systemConfig] = await SystemModel.findOrCreate({
|
||||||
|
where: { type: AuthDataType.systemConfig },
|
||||||
|
});
|
||||||
|
await SystemModel.findOrCreate({
|
||||||
|
where: { type: AuthDataType.notification },
|
||||||
|
});
|
||||||
|
const [authConfig] = await SystemModel.findOrCreate({
|
||||||
|
where: { type: AuthDataType.authConfig },
|
||||||
|
});
|
||||||
|
if (!authConfig?.info) {
|
||||||
|
let authInfo = {
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const authFileExist = await fileExist(config.authConfigFile);
|
||||||
|
if (authFileExist) {
|
||||||
|
const content = await readFile(config.authConfigFile, 'utf8');
|
||||||
|
authInfo = safeJSONParse(content);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.warn('Failed to read auth config file, using default credentials');
|
||||||
|
}
|
||||||
|
await SystemModel.upsert({
|
||||||
|
id: authConfig?.id,
|
||||||
|
info: authInfo,
|
||||||
|
type: AuthDataType.authConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const installDependencies = () => {
|
const installDependencies = async () => {
|
||||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
const docs = await DependenceModel.findAll({
|
||||||
DependenceModel.findAll({
|
|
||||||
where: {},
|
where: {},
|
||||||
order: [
|
order: [
|
||||||
['type', 'DESC'],
|
['type', 'DESC'],
|
||||||
['createdAt', 'DESC'],
|
['createdAt', 'DESC'],
|
||||||
],
|
],
|
||||||
raw: true,
|
raw: true,
|
||||||
}).then(async (docs) => {
|
});
|
||||||
|
|
||||||
await DependenceModel.update(
|
await DependenceModel.update(
|
||||||
{ status: DependenceStatus.queued, log: [] },
|
{ status: DependenceStatus.queued, log: [] },
|
||||||
{ where: { id: docs.map((x) => x.id!) } },
|
{ where: { id: docs.map((x) => x.id!) } },
|
||||||
);
|
);
|
||||||
setTimeout(() => {
|
|
||||||
dependenceService.installDependenceOneByOne(docs);
|
setTimeout(async () => {
|
||||||
|
await dependenceService.installDependenceOneByOne(docs);
|
||||||
|
|
||||||
|
const bootAfterLoader = await import('./bootAfter');
|
||||||
|
bootAfterLoader.default();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化更新 linux/python/nodejs 镜像源配置
|
// 初始化更新 linux/python/nodejs 镜像源配置
|
||||||
const systemConfig = await systemService.getSystemConfig();
|
|
||||||
if (systemConfig.info?.pythonMirror) {
|
if (systemConfig.info?.pythonMirror) {
|
||||||
systemService.updatePythonMirror({
|
systemService.updatePythonMirror({
|
||||||
pythonMirror: systemConfig.info?.pythonMirror,
|
pythonMirror: systemConfig.info?.pythonMirror,
|
||||||
@@ -162,4 +213,11 @@ export default async () => {
|
|||||||
// 初始化保存一次ck和定时任务数据
|
// 初始化保存一次ck和定时任务数据
|
||||||
await cronService.autosave_crontab();
|
await cronService.autosave_crontab();
|
||||||
await envService.set_envs();
|
await envService.set_envs();
|
||||||
|
|
||||||
|
const authInfo = await userService.getAuthInfo();
|
||||||
|
const apps = await openService.findApps();
|
||||||
|
await shareStore.updateAuthInfo(authInfo);
|
||||||
|
if (apps?.length) {
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+81
-106
@@ -3,6 +3,7 @@ import path from 'path';
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { fileExist } from '../config/util';
|
import { fileExist } from '../config/util';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
const rootPath = process.env.QL_DIR as string;
|
const rootPath = process.env.QL_DIR as string;
|
||||||
let dataPath = path.join(rootPath, 'data/');
|
let dataPath = path.join(rootPath, 'data/');
|
||||||
@@ -20,16 +21,14 @@ const bakPath = path.join(dataPath, 'bak/');
|
|||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
const tmpPath = path.join(logPath, '.tmp/');
|
const tmpPath = path.join(logPath, '.tmp/');
|
||||||
const confFile = path.join(configPath, 'config.sh');
|
const confFile = path.join(configPath, 'config.sh');
|
||||||
const authConfigFile = path.join(configPath, 'auth.json');
|
|
||||||
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
||||||
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
|
|
||||||
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
|
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
|
||||||
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
|
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
|
||||||
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
|
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
|
||||||
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
|
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
|
||||||
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
|
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
|
||||||
const jsNotifyFile = path.join(preloadPath, 'notify.js');
|
const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
|
||||||
const pyNotifyFile = path.join(preloadPath, 'notify.py');
|
const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
|
||||||
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
|
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
|
||||||
const TaskBeforeJsFile = path.join(configPath, 'task_before.js');
|
const TaskBeforeJsFile = path.join(configPath, 'task_before.js');
|
||||||
const TaskBeforePyFile = path.join(configPath, 'task_before.py');
|
const TaskBeforePyFile = path.join(configPath, 'task_before.py');
|
||||||
@@ -39,114 +38,90 @@ const sshPath = path.resolve(homedir, '.ssh');
|
|||||||
const sshdPath = path.join(dataPath, 'ssh.d');
|
const sshdPath = path.join(dataPath, 'ssh.d');
|
||||||
const systemLogPath = path.join(dataPath, 'syslog');
|
const systemLogPath = path.join(dataPath, 'syslog');
|
||||||
|
|
||||||
export default async () => {
|
const directories = [
|
||||||
const authFileExist = await fileExist(authConfigFile);
|
configPath,
|
||||||
const confFileExist = await fileExist(confFile);
|
scriptPath,
|
||||||
const scriptDirExist = await fileExist(scriptPath);
|
preloadPath,
|
||||||
const preloadDirExist = await fileExist(preloadPath);
|
logPath,
|
||||||
const logDirExist = await fileExist(logPath);
|
tmpPath,
|
||||||
const configDirExist = await fileExist(configPath);
|
uploadPath,
|
||||||
const uploadDirExist = await fileExist(uploadPath);
|
sshPath,
|
||||||
const sshDirExist = await fileExist(sshPath);
|
bakPath,
|
||||||
const bakDirExist = await fileExist(bakPath);
|
sshdPath,
|
||||||
const sshdDirExist = await fileExist(sshdPath);
|
systemLogPath,
|
||||||
const systemLogDirExist = await fileExist(systemLogPath);
|
];
|
||||||
const tmpDirExist = await fileExist(tmpPath);
|
|
||||||
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
|
|
||||||
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
|
|
||||||
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
|
|
||||||
const TaskBeforeJsFileExist = await fileExist(TaskBeforeJsFile);
|
|
||||||
const TaskBeforePyFileExist = await fileExist(TaskBeforePyFile);
|
|
||||||
const TaskAfterFileExist = await fileExist(TaskAfterFile);
|
|
||||||
|
|
||||||
if (!configDirExist) {
|
const files = [
|
||||||
await fs.mkdir(configPath);
|
{
|
||||||
}
|
target: confFile,
|
||||||
|
source: sampleConfigFile,
|
||||||
if (!scriptDirExist) {
|
checkExistence: true,
|
||||||
await fs.mkdir(scriptPath);
|
},
|
||||||
}
|
{
|
||||||
|
target: jsNotifyFile,
|
||||||
if (!preloadDirExist) {
|
source: sampleNotifyJsFile,
|
||||||
await fs.mkdir(preloadPath);
|
checkExistence: false,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
if (!logDirExist) {
|
target: pyNotifyFile,
|
||||||
await fs.mkdir(logPath);
|
source: sampleNotifyPyFile,
|
||||||
}
|
checkExistence: false,
|
||||||
|
},
|
||||||
if (!tmpDirExist) {
|
{
|
||||||
await fs.mkdir(tmpPath);
|
target: scriptNotifyJsFile,
|
||||||
}
|
source: sampleNotifyJsFile,
|
||||||
|
checkExistence: true,
|
||||||
if (!uploadDirExist) {
|
},
|
||||||
await fs.mkdir(uploadPath);
|
{
|
||||||
}
|
target: scriptNotifyPyFile,
|
||||||
|
source: sampleNotifyPyFile,
|
||||||
if (!sshDirExist) {
|
checkExistence: true,
|
||||||
await fs.mkdir(sshPath);
|
},
|
||||||
}
|
{
|
||||||
|
target: TaskBeforeFile,
|
||||||
if (!bakDirExist) {
|
source: sampleTaskShellFile,
|
||||||
await fs.mkdir(bakPath);
|
checkExistence: true,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
if (!sshdDirExist) {
|
target: TaskBeforeJsFile,
|
||||||
await fs.mkdir(sshdPath);
|
content:
|
||||||
}
|
|
||||||
|
|
||||||
if (!systemLogDirExist) {
|
|
||||||
await fs.mkdir(systemLogPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化文件
|
|
||||||
if (!authFileExist) {
|
|
||||||
await fs.writeFile(authConfigFile, await fs.readFile(sampleAuthFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confFileExist) {
|
|
||||||
await fs.writeFile(confFile, await fs.readFile(sampleConfigFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(jsNotifyFile, await fs.readFile(sampleNotifyJsFile));
|
|
||||||
await fs.writeFile(pyNotifyFile, await fs.readFile(sampleNotifyPyFile));
|
|
||||||
|
|
||||||
if (!scriptNotifyJsFileExist) {
|
|
||||||
await fs.writeFile(
|
|
||||||
scriptNotifyJsFile,
|
|
||||||
await fs.readFile(sampleNotifyJsFile),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!scriptNotifyPyFileExist) {
|
|
||||||
await fs.writeFile(
|
|
||||||
scriptNotifyPyFile,
|
|
||||||
await fs.readFile(sampleNotifyPyFile),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TaskBeforeFileExist) {
|
|
||||||
await fs.writeFile(TaskBeforeFile, await fs.readFile(sampleTaskShellFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TaskBeforeJsFileExist) {
|
|
||||||
await fs.writeFile(
|
|
||||||
TaskBeforeJsFile,
|
|
||||||
'// The JavaScript code that executes before the JavaScript task execution will execute.',
|
'// The JavaScript code that executes before the JavaScript task execution will execute.',
|
||||||
);
|
checkExistence: true,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
if (!TaskBeforePyFileExist) {
|
target: TaskBeforePyFile,
|
||||||
await fs.writeFile(
|
content:
|
||||||
TaskBeforePyFile,
|
|
||||||
'# The Python code that executes before the Python task execution will execute.',
|
'# The Python code that executes before the Python task execution will execute.',
|
||||||
);
|
checkExistence: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: TaskAfterFile,
|
||||||
|
source: sampleTaskShellFile,
|
||||||
|
checkExistence: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
for (const dirPath of directories) {
|
||||||
|
if (!(await fileExist(dirPath))) {
|
||||||
|
await fs.mkdir(dirPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TaskAfterFileExist) {
|
for (const item of files) {
|
||||||
await fs.writeFile(TaskAfterFile, await fs.readFile(sampleTaskShellFile));
|
const exists = await fileExist(item.target);
|
||||||
|
if (!item.checkExistence || !exists) {
|
||||||
|
if (!item.content && !item.source) {
|
||||||
|
throw new Error(
|
||||||
|
`Neither content nor source specified for ${item.target}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const content =
|
||||||
|
item.content ||
|
||||||
|
(await fs.readFile(item.source!, { encoding: 'utf-8' }));
|
||||||
|
await writeFileWithLock(item.target, content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.info('✌️ Init file down');
|
Logger.info('✌️ Init file down');
|
||||||
console.log('✌️ Init file down');
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ export default async () => {
|
|||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
|
||||||
// 生成内置token
|
// 生成内置token
|
||||||
let tokenCommand = `ts-node-transpile-only ${join(config.rootPath, 'back/token.ts')}`;
|
let tokenCommand = `ts-node-transpile-only ${join(
|
||||||
|
config.rootPath,
|
||||||
|
'back/token.ts',
|
||||||
|
)}`;
|
||||||
const tokenFile = join(config.rootPath, 'static/build/token.js');
|
const tokenFile = join(config.rootPath, 'static/build/token.js');
|
||||||
|
|
||||||
if (await fileExist(tokenFile)) {
|
if (await fileExist(tokenFile)) {
|
||||||
@@ -22,30 +25,43 @@ export default async () => {
|
|||||||
id: NaN,
|
id: NaN,
|
||||||
name: '生成token',
|
name: '生成token',
|
||||||
command: tokenCommand,
|
command: tokenCommand,
|
||||||
|
runOrigin: 'system',
|
||||||
} as ScheduleTaskType;
|
} as ScheduleTaskType;
|
||||||
await scheduleService.cancelIntervalTask(cron);
|
await scheduleService.cancelIntervalTask(cron);
|
||||||
scheduleService.createIntervalTask(cron, {
|
scheduleService.createIntervalTask(
|
||||||
|
cron,
|
||||||
|
{
|
||||||
days: 28,
|
days: 28,
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
// 运行删除日志任务
|
// 运行删除日志任务
|
||||||
const data = await systemService.getSystemConfig();
|
const data = await systemService.getSystemConfig();
|
||||||
if (data && data.info && data.info.logRemoveFrequency) {
|
if (data && data.info) {
|
||||||
|
if (data.info.logRemoveFrequency) {
|
||||||
const rmlogCron = {
|
const rmlogCron = {
|
||||||
id: data.id as number,
|
id: data.id as number,
|
||||||
name: '删除日志',
|
name: '删除日志',
|
||||||
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||||
|
runOrigin: 'system' as const,
|
||||||
};
|
};
|
||||||
await scheduleService.cancelIntervalTask(rmlogCron);
|
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||||
scheduleService.createIntervalTask(rmlogCron, {
|
scheduleService.createIntervalTask(
|
||||||
|
rmlogCron,
|
||||||
|
{
|
||||||
days: data.info.logRemoveFrequency,
|
days: data.info.logRemoveFrequency,
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
systemService.updateTimezone(data.info);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行所有订阅
|
|
||||||
await subscriptionService.setSshConfig();
|
await subscriptionService.setSshConfig();
|
||||||
const subs = await subscriptionService.list();
|
const subs = await subscriptionService.list();
|
||||||
for (const sub of subs) {
|
for (const sub of subs) {
|
||||||
subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
|
subscriptionService.handleTask(sub.get({ plain: true }), !sub.is_disabled);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+51
-26
@@ -4,35 +4,60 @@ import config from '../config';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const levelMap: Record<string, string> = {
|
const levelMap: Record<string, string> = {
|
||||||
info: '🔵',
|
info: 'ℹ️', // info图标
|
||||||
warn: '🟡',
|
warn: '⚠️', // 警告图标
|
||||||
error: '🔴',
|
error: '❌', // 错误图标
|
||||||
debug: '🔶'
|
debug: '🐛', // 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({
|
const baseFormat = [
|
||||||
level: config.logs.level,
|
winston.format.splat(),
|
||||||
levels: winston.config.npm.levels,
|
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
transports: [
|
winston.format.align(),
|
||||||
new winston.transports.DailyRotateFile({
|
];
|
||||||
|
|
||||||
|
const consoleFormat = winston.format.combine(
|
||||||
|
winston.format.colorize({ level: true }),
|
||||||
|
...baseFormat,
|
||||||
|
winston.format.printf((info) => {
|
||||||
|
return `[${info.level} ${info.timestamp}]:${info.message}`;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const plainFormat = winston.format.combine(
|
||||||
|
winston.format.uncolorize(),
|
||||||
|
...baseFormat,
|
||||||
|
winston.format.printf((info) => {
|
||||||
|
return `[${levelMap[info.level] || ''}${info.level} ${info.timestamp}]:${
|
||||||
|
info.message
|
||||||
|
}`;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const consoleTransport = new winston.transports.Console({
|
||||||
|
format: consoleFormat,
|
||||||
|
level: 'debug',
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileTransport = new winston.transports.DailyRotateFile({
|
||||||
filename: path.join(config.systemLogPath, '%DATE%.log'),
|
filename: path.join(config.systemLogPath, '%DATE%.log'),
|
||||||
...defaultOptions,
|
datePattern: 'YYYY-MM-DD',
|
||||||
})
|
maxSize: '20m',
|
||||||
],
|
maxFiles: '7d',
|
||||||
|
format: plainFormat,
|
||||||
|
level: config.logs.level || 'info',
|
||||||
|
});
|
||||||
|
|
||||||
|
const LoggerInstance = winston.createLogger({
|
||||||
|
level: 'debug',
|
||||||
|
levels: winston.config.npm.levels,
|
||||||
|
transports: [consoleTransport, fileTransport],
|
||||||
|
exceptionHandlers: [consoleTransport, fileTransport],
|
||||||
|
rejectionHandlers: [consoleTransport, fileTransport],
|
||||||
|
});
|
||||||
|
|
||||||
|
LoggerInstance.on('error', (error) => {
|
||||||
|
console.error('Logger error:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LoggerInstance;
|
export default LoggerInstance;
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Application } from 'express';
|
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import Logger from './logger';
|
|
||||||
import config from '../config';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { parseVersion } from '../config/util';
|
|
||||||
|
|
||||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
|
||||||
const { version } = await parseVersion(config.versionFile);
|
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
ignoreErrors: [
|
|
||||||
/SequelizeUniqueConstraintError/i,
|
|
||||||
/Validation error/i,
|
|
||||||
/UnauthorizedError/i,
|
|
||||||
/celebrate request validation failed/i,
|
|
||||||
],
|
|
||||||
dsn: 'https://8b5c84cfef3e22541bc84de0ed00497b@o1098464.ingest.sentry.io/6122819',
|
|
||||||
integrations: [
|
|
||||||
new Sentry.Integrations.Http({ tracing: true }),
|
|
||||||
new Sentry.Integrations.Express({ app: expressApp }),
|
|
||||||
],
|
|
||||||
tracesSampleRate: 0.8,
|
|
||||||
release: version,
|
|
||||||
});
|
|
||||||
|
|
||||||
expressApp.use(Sentry.Handlers.requestHandler());
|
|
||||||
expressApp.use(Sentry.Handlers.tracingHandler());
|
|
||||||
|
|
||||||
Logger.info('✌️ Sentry loaded');
|
|
||||||
console.log('✌️ Sentry loaded');
|
|
||||||
};
|
|
||||||
@@ -2,9 +2,8 @@ import sockJs from 'sockjs';
|
|||||||
import { Server } from 'http';
|
import { Server } from 'http';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import SockService from '../services/sock';
|
import SockService from '../services/sock';
|
||||||
import config from '../config/index';
|
import { getPlatform } from '../config/util';
|
||||||
import fs from 'fs/promises';
|
import { shareStore } from '../shared/store';
|
||||||
import { getPlatform, safeJSONParse } from '../config/util';
|
|
||||||
|
|
||||||
export default async ({ server }: { server: Server }) => {
|
export default async ({ server }: { server: Server }) => {
|
||||||
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||||
@@ -15,13 +14,12 @@ export default async ({ server }: { server: Server }) => {
|
|||||||
conn.close('404');
|
conn.close('404');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fs.readFile(config.authConfigFile, 'utf8');
|
const authInfo = await shareStore.getAuthInfo();
|
||||||
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||||
if (data) {
|
if (authInfo) {
|
||||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
const { token = '', tokens = {} } = authInfo;
|
||||||
if (headerToken === token || tokens[platform] === headerToken) {
|
if (headerToken === token || tokens[platform] === headerToken) {
|
||||||
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
|
||||||
sockService.addClient(conn);
|
sockService.addClient(conn);
|
||||||
|
|
||||||
conn.on('data', (message) => {
|
conn.on('data', (message) => {
|
||||||
@@ -33,8 +31,6 @@ export default async ({ server }: { server: Server }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
conn.write(JSON.stringify({ type: 'ping', message: 'whyour' }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import { performance } from 'perf_hooks';
|
||||||
|
import { metricsService } from '../services/metrics';
|
||||||
|
|
||||||
|
interface RequestMetrics {
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
duration: number;
|
||||||
|
statusCode: number;
|
||||||
|
timestamp: number;
|
||||||
|
platform?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestMetrics: RequestMetrics[] = [];
|
||||||
|
|
||||||
|
export const monitoringMiddleware = (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) => {
|
||||||
|
const start = performance.now();
|
||||||
|
const originalEnd = res.end;
|
||||||
|
|
||||||
|
res.end = function (chunk?: any, encoding?: any, cb?: any) {
|
||||||
|
const duration = performance.now() - start;
|
||||||
|
const metric: RequestMetrics = {
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
duration,
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
platform: req.platform,
|
||||||
|
};
|
||||||
|
|
||||||
|
requestMetrics.push(metric);
|
||||||
|
metricsService.record('http_request', duration, {
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
statusCode: res.statusCode.toString(),
|
||||||
|
...(req.platform && { platform: req.platform }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requestMetrics.length > 1000) {
|
||||||
|
requestMetrics.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration > 1000) {
|
||||||
|
Logger.warn(
|
||||||
|
`Slow request detected: ${req.method} ${
|
||||||
|
req.path
|
||||||
|
} took ${duration.toFixed(2)}ms`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalEnd.call(this, chunk, encoding, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMetrics = () => {
|
||||||
|
return {
|
||||||
|
totalRequests: requestMetrics.length,
|
||||||
|
averageDuration:
|
||||||
|
requestMetrics.reduce((acc, curr) => acc + curr.duration, 0) /
|
||||||
|
requestMetrics.length,
|
||||||
|
requestsByMethod: requestMetrics.reduce((acc, curr) => {
|
||||||
|
acc[curr.method] = (acc[curr.method] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>),
|
||||||
|
requestsByPlatform: requestMetrics.reduce((acc, curr) => {
|
||||||
|
if (curr.platform) {
|
||||||
|
acc[curr.platform] = (acc[curr.platform] || 0) + 1;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>),
|
||||||
|
recentRequests: requestMetrics.slice(-10),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package com.ql.api;
|
||||||
|
|
||||||
|
message EnvItem {
|
||||||
|
optional int32 id = 1;
|
||||||
|
optional string name = 2;
|
||||||
|
optional string value = 3;
|
||||||
|
optional string remarks = 4;
|
||||||
|
optional int32 status = 5;
|
||||||
|
optional int64 position = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetEnvsRequest { string searchValue = 1; }
|
||||||
|
|
||||||
|
message CreateEnvRequest { repeated EnvItem envs = 1; }
|
||||||
|
|
||||||
|
message UpdateEnvRequest { EnvItem env = 1; }
|
||||||
|
|
||||||
|
message DeleteEnvsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message MoveEnvRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
int32 fromIndex = 2;
|
||||||
|
int32 toIndex = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DisableEnvsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message EnableEnvsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message UpdateEnvNamesRequest {
|
||||||
|
repeated int32 ids = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetEnvByIdRequest { int32 id = 1; }
|
||||||
|
|
||||||
|
message EnvsResponse {
|
||||||
|
int32 code = 1;
|
||||||
|
repeated EnvItem data = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EnvResponse {
|
||||||
|
int32 code = 1;
|
||||||
|
EnvItem data = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
int32 code = 1;
|
||||||
|
optional string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExtraScheduleItem { string schedule = 1; }
|
||||||
|
|
||||||
|
message CronItem {
|
||||||
|
optional int32 id = 1;
|
||||||
|
optional string command = 2;
|
||||||
|
optional string schedule = 3;
|
||||||
|
optional string name = 4;
|
||||||
|
repeated string labels = 5;
|
||||||
|
optional int32 sub_id = 6;
|
||||||
|
repeated ExtraScheduleItem extra_schedules = 7;
|
||||||
|
optional string task_before = 8;
|
||||||
|
optional string task_after = 9;
|
||||||
|
optional int32 status = 10;
|
||||||
|
optional string log_path = 11;
|
||||||
|
optional int32 pid = 12;
|
||||||
|
optional int64 last_running_time = 13;
|
||||||
|
optional int64 last_execution_time = 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateCronRequest {
|
||||||
|
string command = 1;
|
||||||
|
string schedule = 2;
|
||||||
|
optional string name = 3;
|
||||||
|
repeated string labels = 4;
|
||||||
|
optional int32 sub_id = 5;
|
||||||
|
repeated ExtraScheduleItem extra_schedules = 6;
|
||||||
|
optional string task_before = 7;
|
||||||
|
optional string task_after = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateCronRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
optional string command = 2;
|
||||||
|
optional string schedule = 3;
|
||||||
|
optional string name = 4;
|
||||||
|
repeated string labels = 5;
|
||||||
|
optional int32 sub_id = 6;
|
||||||
|
repeated ExtraScheduleItem extra_schedules = 7;
|
||||||
|
optional string task_before = 8;
|
||||||
|
optional string task_after = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteCronsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message CronsResponse {
|
||||||
|
int32 code = 1;
|
||||||
|
repeated CronItem data = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CronResponse {
|
||||||
|
int32 code = 1;
|
||||||
|
CronItem data = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CronDetailRequest { string log_path = 1; }
|
||||||
|
|
||||||
|
message CronDetailResponse {
|
||||||
|
int32 code = 1;
|
||||||
|
CronItem data = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotificationMode {
|
||||||
|
gotify = 0;
|
||||||
|
goCqHttpBot = 1;
|
||||||
|
serverChan = 2;
|
||||||
|
pushDeer = 3;
|
||||||
|
bark = 4;
|
||||||
|
chat = 5;
|
||||||
|
telegramBot = 6;
|
||||||
|
dingtalkBot = 7;
|
||||||
|
weWorkBot = 8;
|
||||||
|
weWorkApp = 9;
|
||||||
|
aibotk = 10;
|
||||||
|
iGot = 11;
|
||||||
|
pushPlus = 12;
|
||||||
|
wePlusBot = 13;
|
||||||
|
email = 14;
|
||||||
|
pushMe = 15;
|
||||||
|
feishu = 16;
|
||||||
|
webhook = 17;
|
||||||
|
chronocat = 18;
|
||||||
|
ntfy = 19;
|
||||||
|
wxPusherBot = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NotificationInfo {
|
||||||
|
NotificationMode type = 1;
|
||||||
|
|
||||||
|
optional string gotifyUrl = 2;
|
||||||
|
optional string gotifyToken = 3;
|
||||||
|
optional int32 gotifyPriority = 4;
|
||||||
|
|
||||||
|
optional string goCqHttpBotUrl = 5;
|
||||||
|
optional string goCqHttpBotToken = 6;
|
||||||
|
optional string goCqHttpBotQq = 7;
|
||||||
|
|
||||||
|
optional string serverChanKey = 8;
|
||||||
|
|
||||||
|
optional string pushDeerKey = 9;
|
||||||
|
optional string pushDeerUrl = 10;
|
||||||
|
|
||||||
|
optional string synologyChatUrl = 11;
|
||||||
|
|
||||||
|
optional string barkPush = 12;
|
||||||
|
optional string barkIcon = 13;
|
||||||
|
optional string barkSound = 14;
|
||||||
|
optional string barkGroup = 15;
|
||||||
|
optional string barkLevel = 16;
|
||||||
|
optional string barkUrl = 17;
|
||||||
|
optional string barkArchive = 18;
|
||||||
|
|
||||||
|
optional string telegramBotToken = 19;
|
||||||
|
optional string telegramBotUserId = 20;
|
||||||
|
optional string telegramBotProxyHost = 21;
|
||||||
|
optional string telegramBotProxyPort = 22;
|
||||||
|
optional string telegramBotProxyAuth = 23;
|
||||||
|
optional string telegramBotApiHost = 24;
|
||||||
|
|
||||||
|
optional string dingtalkBotToken = 25;
|
||||||
|
optional string dingtalkBotSecret = 26;
|
||||||
|
|
||||||
|
optional string weWorkBotKey = 27;
|
||||||
|
optional string weWorkOrigin = 28;
|
||||||
|
|
||||||
|
optional string weWorkAppKey = 29;
|
||||||
|
|
||||||
|
optional string aibotkKey = 30;
|
||||||
|
optional string aibotkType = 31;
|
||||||
|
optional string aibotkName = 32;
|
||||||
|
|
||||||
|
optional string iGotPushKey = 33;
|
||||||
|
|
||||||
|
optional string pushPlusToken = 34;
|
||||||
|
optional string pushPlusUser = 35;
|
||||||
|
optional string pushPlusTemplate = 36;
|
||||||
|
optional string pushplusChannel = 37;
|
||||||
|
optional string pushplusWebhook = 38;
|
||||||
|
optional string pushplusCallbackUrl = 39;
|
||||||
|
optional string pushplusTo = 40;
|
||||||
|
|
||||||
|
optional string wePlusBotToken = 41;
|
||||||
|
optional string wePlusBotReceiver = 42;
|
||||||
|
optional string wePlusBotVersion = 43;
|
||||||
|
|
||||||
|
optional string emailService = 44;
|
||||||
|
optional string emailUser = 45;
|
||||||
|
optional string emailPass = 46;
|
||||||
|
optional string emailTo = 47;
|
||||||
|
|
||||||
|
optional string pushMeKey = 48;
|
||||||
|
optional string pushMeUrl = 49;
|
||||||
|
|
||||||
|
optional string chronocatURL = 50;
|
||||||
|
optional string chronocatQQ = 51;
|
||||||
|
optional string chronocatToken = 52;
|
||||||
|
|
||||||
|
optional string webhookHeaders = 53;
|
||||||
|
optional string webhookBody = 54;
|
||||||
|
optional string webhookUrl = 55;
|
||||||
|
optional string webhookMethod = 56;
|
||||||
|
optional string webhookContentType = 57;
|
||||||
|
|
||||||
|
optional string larkKey = 58;
|
||||||
|
|
||||||
|
optional string ntfyUrl = 59;
|
||||||
|
optional string ntfyTopic = 60;
|
||||||
|
optional string ntfyPriority = 61;
|
||||||
|
optional string ntfyToken = 62;
|
||||||
|
optional string ntfyUsername = 63;
|
||||||
|
optional string ntfyPassword = 64;
|
||||||
|
optional string ntfyActions = 65;
|
||||||
|
|
||||||
|
optional string wxPusherBotAppToken = 66;
|
||||||
|
optional string wxPusherBotTopicIds = 67;
|
||||||
|
optional string wxPusherBotUids = 68;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SystemNotifyRequest {
|
||||||
|
string title = 1;
|
||||||
|
string content = 2;
|
||||||
|
optional NotificationInfo notificationInfo = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Api {
|
||||||
|
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
|
||||||
|
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
|
||||||
|
rpc UpdateEnv(UpdateEnvRequest) returns (EnvResponse) {}
|
||||||
|
rpc DeleteEnvs(DeleteEnvsRequest) returns (Response) {}
|
||||||
|
rpc MoveEnv(MoveEnvRequest) returns (EnvResponse) {}
|
||||||
|
rpc DisableEnvs(DisableEnvsRequest) returns (Response) {}
|
||||||
|
rpc EnableEnvs(EnableEnvsRequest) returns (Response) {}
|
||||||
|
rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}
|
||||||
|
rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}
|
||||||
|
rpc SystemNotify(SystemNotifyRequest) returns (Response) {}
|
||||||
|
rpc GetCronDetail(CronDetailRequest) returns (CronDetailResponse) {}
|
||||||
|
rpc CreateCron(CreateCronRequest) returns (CronResponse) {}
|
||||||
|
rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}
|
||||||
|
rpc DeleteCrons(DeleteCronsRequest) returns (Response) {}
|
||||||
|
}
|
||||||
+4251
File diff suppressed because it is too large
Load Diff
+114
-91
@@ -1,17 +1,23 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.6.1
|
||||||
|
// protoc v3.17.3
|
||||||
|
// source: back/protos/cron.proto
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
import {
|
import {
|
||||||
CallOptions,
|
type CallOptions,
|
||||||
ChannelCredentials,
|
ChannelCredentials,
|
||||||
Client,
|
Client,
|
||||||
ClientOptions,
|
type ClientOptions,
|
||||||
ClientUnaryCall,
|
type ClientUnaryCall,
|
||||||
handleUnaryCall,
|
type handleUnaryCall,
|
||||||
makeGenericClientConstructor,
|
makeGenericClientConstructor,
|
||||||
Metadata,
|
Metadata,
|
||||||
ServiceError,
|
type ServiceError,
|
||||||
UntypedServiceImplementation,
|
type UntypedServiceImplementation,
|
||||||
} from "@grpc/grpc-js";
|
} from "@grpc/grpc-js";
|
||||||
import _m0 from "protobufjs/minimal";
|
|
||||||
|
|
||||||
export const protobufPackage = "com.ql.cron";
|
export const protobufPackage = "com.ql.cron";
|
||||||
|
|
||||||
@@ -23,7 +29,7 @@ export interface ICron {
|
|||||||
id: string;
|
id: string;
|
||||||
schedule: string;
|
schedule: string;
|
||||||
command: string;
|
command: string;
|
||||||
extraSchedules: ISchedule[];
|
extra_schedules: ISchedule[];
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,22 +51,22 @@ function createBaseISchedule(): ISchedule {
|
|||||||
return { schedule: "" };
|
return { schedule: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ISchedule = {
|
export const ISchedule: MessageFns<ISchedule> = {
|
||||||
encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
encode(message: ISchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
if (message.schedule !== "") {
|
if (message.schedule !== "") {
|
||||||
writer.uint32(10).string(message.schedule);
|
writer.uint32(10).string(message.schedule);
|
||||||
}
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: _m0.Reader | Uint8Array, length?: number): ISchedule {
|
decode(input: BinaryReader | Uint8Array, length?: number): ISchedule {
|
||||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseISchedule();
|
const message = createBaseISchedule();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
const tag = reader.uint32();
|
const tag = reader.uint32();
|
||||||
switch (tag >>> 3) {
|
switch (tag >>> 3) {
|
||||||
case 1:
|
case 1: {
|
||||||
if (tag !== 10) {
|
if (tag !== 10) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -68,28 +74,30 @@ export const ISchedule = {
|
|||||||
message.schedule = reader.string();
|
message.schedule = reader.string();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
reader.skipType(tag & 7);
|
reader.skip(tag & 7);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
|
||||||
fromJSON(object: any): ISchedule {
|
fromJSON(object: any): ISchedule {
|
||||||
return { schedule: isSet(object.schedule) ? String(object.schedule) : "" };
|
return { schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "" };
|
||||||
},
|
},
|
||||||
|
|
||||||
toJSON(message: ISchedule): unknown {
|
toJSON(message: ISchedule): unknown {
|
||||||
const obj: any = {};
|
const obj: any = {};
|
||||||
message.schedule !== undefined && (obj.schedule = message.schedule);
|
if (message.schedule !== "") {
|
||||||
|
obj.schedule = message.schedule;
|
||||||
|
}
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
|
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
|
||||||
return ISchedule.fromPartial(base ?? {});
|
return ISchedule.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
|
|
||||||
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
|
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
|
||||||
const message = createBaseISchedule();
|
const message = createBaseISchedule();
|
||||||
message.schedule = object.schedule ?? "";
|
message.schedule = object.schedule ?? "";
|
||||||
@@ -98,11 +106,11 @@ export const ISchedule = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function createBaseICron(): ICron {
|
function createBaseICron(): ICron {
|
||||||
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
|
return { id: "", schedule: "", command: "", extra_schedules: [], name: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ICron = {
|
export const ICron: MessageFns<ICron> = {
|
||||||
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
encode(message: ICron, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
if (message.id !== "") {
|
if (message.id !== "") {
|
||||||
writer.uint32(10).string(message.id);
|
writer.uint32(10).string(message.id);
|
||||||
}
|
}
|
||||||
@@ -112,8 +120,8 @@ export const ICron = {
|
|||||||
if (message.command !== "") {
|
if (message.command !== "") {
|
||||||
writer.uint32(26).string(message.command);
|
writer.uint32(26).string(message.command);
|
||||||
}
|
}
|
||||||
for (const v of message.extraSchedules) {
|
for (const v of message.extra_schedules) {
|
||||||
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
|
ISchedule.encode(v!, writer.uint32(34).fork()).join();
|
||||||
}
|
}
|
||||||
if (message.name !== "") {
|
if (message.name !== "") {
|
||||||
writer.uint32(42).string(message.name);
|
writer.uint32(42).string(message.name);
|
||||||
@@ -121,42 +129,46 @@ export const ICron = {
|
|||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
|
decode(input: BinaryReader | Uint8Array, length?: number): ICron {
|
||||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseICron();
|
const message = createBaseICron();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
const tag = reader.uint32();
|
const tag = reader.uint32();
|
||||||
switch (tag >>> 3) {
|
switch (tag >>> 3) {
|
||||||
case 1:
|
case 1: {
|
||||||
if (tag !== 10) {
|
if (tag !== 10) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
message.id = reader.string();
|
message.id = reader.string();
|
||||||
continue;
|
continue;
|
||||||
case 2:
|
}
|
||||||
|
case 2: {
|
||||||
if (tag !== 18) {
|
if (tag !== 18) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
message.schedule = reader.string();
|
message.schedule = reader.string();
|
||||||
continue;
|
continue;
|
||||||
case 3:
|
}
|
||||||
|
case 3: {
|
||||||
if (tag !== 26) {
|
if (tag !== 26) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
message.command = reader.string();
|
message.command = reader.string();
|
||||||
continue;
|
continue;
|
||||||
case 4:
|
}
|
||||||
|
case 4: {
|
||||||
if (tag !== 34) {
|
if (tag !== 34) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
|
message.extra_schedules.push(ISchedule.decode(reader, reader.uint32()));
|
||||||
continue;
|
continue;
|
||||||
case 5:
|
}
|
||||||
|
case 5: {
|
||||||
if (tag !== 42) {
|
if (tag !== 42) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -164,50 +176,56 @@ export const ICron = {
|
|||||||
message.name = reader.string();
|
message.name = reader.string();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
reader.skipType(tag & 7);
|
reader.skip(tag & 7);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
|
||||||
fromJSON(object: any): ICron {
|
fromJSON(object: any): ICron {
|
||||||
return {
|
return {
|
||||||
id: isSet(object.id) ? String(object.id) : "",
|
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||||
schedule: isSet(object.schedule) ? String(object.schedule) : "",
|
schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "",
|
||||||
command: isSet(object.command) ? String(object.command) : "",
|
command: isSet(object.command) ? globalThis.String(object.command) : "",
|
||||||
extraSchedules: Array.isArray(object?.extraSchedules)
|
extra_schedules: globalThis.Array.isArray(object?.extra_schedules)
|
||||||
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
|
? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e))
|
||||||
: [],
|
: [],
|
||||||
name: isSet(object.name) ? String(object.name) : "",
|
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
toJSON(message: ICron): unknown {
|
toJSON(message: ICron): unknown {
|
||||||
const obj: any = {};
|
const obj: any = {};
|
||||||
message.id !== undefined && (obj.id = message.id);
|
if (message.id !== "") {
|
||||||
message.schedule !== undefined && (obj.schedule = message.schedule);
|
obj.id = message.id;
|
||||||
message.command !== undefined && (obj.command = message.command);
|
}
|
||||||
if (message.extraSchedules) {
|
if (message.schedule !== "") {
|
||||||
obj.extraSchedules = message.extraSchedules.map((e) => e ? ISchedule.toJSON(e) : undefined);
|
obj.schedule = message.schedule;
|
||||||
} else {
|
}
|
||||||
obj.extraSchedules = [];
|
if (message.command !== "") {
|
||||||
|
obj.command = message.command;
|
||||||
|
}
|
||||||
|
if (message.extra_schedules?.length) {
|
||||||
|
obj.extra_schedules = message.extra_schedules.map((e) => ISchedule.toJSON(e));
|
||||||
|
}
|
||||||
|
if (message.name !== "") {
|
||||||
|
obj.name = message.name;
|
||||||
}
|
}
|
||||||
message.name !== undefined && (obj.name = message.name);
|
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
|
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
|
||||||
return ICron.fromPartial(base ?? {});
|
return ICron.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
|
|
||||||
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
|
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
|
||||||
const message = createBaseICron();
|
const message = createBaseICron();
|
||||||
message.id = object.id ?? "";
|
message.id = object.id ?? "";
|
||||||
message.schedule = object.schedule ?? "";
|
message.schedule = object.schedule ?? "";
|
||||||
message.command = object.command ?? "";
|
message.command = object.command ?? "";
|
||||||
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
message.extra_schedules = object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
||||||
message.name = object.name ?? "";
|
message.name = object.name ?? "";
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
@@ -217,22 +235,22 @@ function createBaseAddCronRequest(): AddCronRequest {
|
|||||||
return { crons: [] };
|
return { crons: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddCronRequest = {
|
export const AddCronRequest: MessageFns<AddCronRequest> = {
|
||||||
encode(message: AddCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
encode(message: AddCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
for (const v of message.crons) {
|
for (const v of message.crons) {
|
||||||
ICron.encode(v!, writer.uint32(10).fork()).ldelim();
|
ICron.encode(v!, writer.uint32(10).fork()).join();
|
||||||
}
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
|
decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest {
|
||||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseAddCronRequest();
|
const message = createBaseAddCronRequest();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
const tag = reader.uint32();
|
const tag = reader.uint32();
|
||||||
switch (tag >>> 3) {
|
switch (tag >>> 3) {
|
||||||
case 1:
|
case 1: {
|
||||||
if (tag !== 10) {
|
if (tag !== 10) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -240,32 +258,30 @@ export const AddCronRequest = {
|
|||||||
message.crons.push(ICron.decode(reader, reader.uint32()));
|
message.crons.push(ICron.decode(reader, reader.uint32()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
reader.skipType(tag & 7);
|
reader.skip(tag & 7);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
|
||||||
fromJSON(object: any): AddCronRequest {
|
fromJSON(object: any): AddCronRequest {
|
||||||
return { crons: Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
|
return { crons: globalThis.Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
|
||||||
},
|
},
|
||||||
|
|
||||||
toJSON(message: AddCronRequest): unknown {
|
toJSON(message: AddCronRequest): unknown {
|
||||||
const obj: any = {};
|
const obj: any = {};
|
||||||
if (message.crons) {
|
if (message.crons?.length) {
|
||||||
obj.crons = message.crons.map((e) => e ? ICron.toJSON(e) : undefined);
|
obj.crons = message.crons.map((e) => ICron.toJSON(e));
|
||||||
} else {
|
|
||||||
obj.crons = [];
|
|
||||||
}
|
}
|
||||||
return obj;
|
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 ?? {});
|
return AddCronRequest.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
|
|
||||||
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
|
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
|
||||||
const message = createBaseAddCronRequest();
|
const message = createBaseAddCronRequest();
|
||||||
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
|
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
|
||||||
@@ -277,13 +293,13 @@ function createBaseAddCronResponse(): AddCronResponse {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddCronResponse = {
|
export const AddCronResponse: MessageFns<AddCronResponse> = {
|
||||||
encode(_: AddCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
encode(_: AddCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
|
decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse {
|
||||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseAddCronResponse();
|
const message = createBaseAddCronResponse();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
@@ -293,7 +309,7 @@ export const AddCronResponse = {
|
|||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
reader.skipType(tag & 7);
|
reader.skip(tag & 7);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
@@ -308,9 +324,8 @@ export const AddCronResponse = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
|
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
|
||||||
return AddCronResponse.fromPartial(base ?? {});
|
return AddCronResponse.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
|
|
||||||
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
|
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
|
||||||
const message = createBaseAddCronResponse();
|
const message = createBaseAddCronResponse();
|
||||||
return message;
|
return message;
|
||||||
@@ -321,22 +336,22 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
|
|||||||
return { ids: [] };
|
return { ids: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteCronRequest = {
|
export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
|
||||||
encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
encode(message: DeleteCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
for (const v of message.ids) {
|
for (const v of message.ids) {
|
||||||
writer.uint32(10).string(v!);
|
writer.uint32(10).string(v!);
|
||||||
}
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
|
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest {
|
||||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseDeleteCronRequest();
|
const message = createBaseDeleteCronRequest();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
const tag = reader.uint32();
|
const tag = reader.uint32();
|
||||||
switch (tag >>> 3) {
|
switch (tag >>> 3) {
|
||||||
case 1:
|
case 1: {
|
||||||
if (tag !== 10) {
|
if (tag !== 10) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -344,32 +359,30 @@ export const DeleteCronRequest = {
|
|||||||
message.ids.push(reader.string());
|
message.ids.push(reader.string());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
reader.skipType(tag & 7);
|
reader.skip(tag & 7);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
|
||||||
fromJSON(object: any): DeleteCronRequest {
|
fromJSON(object: any): DeleteCronRequest {
|
||||||
return { ids: Array.isArray(object?.ids) ? object.ids.map((e: any) => String(e)) : [] };
|
return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.String(e)) : [] };
|
||||||
},
|
},
|
||||||
|
|
||||||
toJSON(message: DeleteCronRequest): unknown {
|
toJSON(message: DeleteCronRequest): unknown {
|
||||||
const obj: any = {};
|
const obj: any = {};
|
||||||
if (message.ids) {
|
if (message.ids?.length) {
|
||||||
obj.ids = message.ids.map((e) => e);
|
obj.ids = message.ids;
|
||||||
} else {
|
|
||||||
obj.ids = [];
|
|
||||||
}
|
}
|
||||||
return obj;
|
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 ?? {});
|
return DeleteCronRequest.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
|
|
||||||
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
|
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
|
||||||
const message = createBaseDeleteCronRequest();
|
const message = createBaseDeleteCronRequest();
|
||||||
message.ids = object.ids?.map((e) => e) || [];
|
message.ids = object.ids?.map((e) => e) || [];
|
||||||
@@ -381,13 +394,13 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteCronResponse = {
|
export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
|
||||||
encode(_: DeleteCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
encode(_: DeleteCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
|
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronResponse {
|
||||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseDeleteCronResponse();
|
const message = createBaseDeleteCronResponse();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
@@ -397,7 +410,7 @@ export const DeleteCronResponse = {
|
|||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
reader.skipType(tag & 7);
|
reader.skip(tag & 7);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
@@ -412,9 +425,8 @@ export const DeleteCronResponse = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
|
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
|
||||||
return DeleteCronResponse.fromPartial(base ?? {});
|
return DeleteCronResponse.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
|
|
||||||
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
|
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
|
||||||
const message = createBaseDeleteCronResponse();
|
const message = createBaseDeleteCronResponse();
|
||||||
return message;
|
return message;
|
||||||
@@ -484,12 +496,14 @@ export interface CronClient extends Client {
|
|||||||
export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as {
|
export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as {
|
||||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;
|
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;
|
||||||
service: typeof CronService;
|
service: typeof CronService;
|
||||||
|
serviceName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
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 globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
: Partial<T>;
|
: Partial<T>;
|
||||||
|
|
||||||
@@ -500,3 +514,12 @@ export type Exact<P, I extends P> = P extends Builtin ? P
|
|||||||
function isSet(value: any): boolean {
|
function isSet(value: any): boolean {
|
||||||
return value !== null && value !== undefined;
|
return value !== null && value !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||||
|
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||||
|
}
|
||||||
|
|||||||
+50
-29
@@ -1,19 +1,25 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.6.1
|
||||||
|
// protoc v3.17.3
|
||||||
|
// source: back/protos/health.proto
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
import {
|
import {
|
||||||
CallOptions,
|
type CallOptions,
|
||||||
ChannelCredentials,
|
ChannelCredentials,
|
||||||
Client,
|
Client,
|
||||||
ClientOptions,
|
type ClientOptions,
|
||||||
ClientReadableStream,
|
type ClientReadableStream,
|
||||||
ClientUnaryCall,
|
type ClientUnaryCall,
|
||||||
handleServerStreamingCall,
|
type handleServerStreamingCall,
|
||||||
handleUnaryCall,
|
type handleUnaryCall,
|
||||||
makeGenericClientConstructor,
|
makeGenericClientConstructor,
|
||||||
Metadata,
|
Metadata,
|
||||||
ServiceError,
|
type ServiceError,
|
||||||
UntypedServiceImplementation,
|
type UntypedServiceImplementation,
|
||||||
} from "@grpc/grpc-js";
|
} from "@grpc/grpc-js";
|
||||||
import _m0 from "protobufjs/minimal";
|
|
||||||
|
|
||||||
export const protobufPackage = "com.ql.health";
|
export const protobufPackage = "com.ql.health";
|
||||||
|
|
||||||
@@ -74,22 +80,22 @@ function createBaseHealthCheckRequest(): HealthCheckRequest {
|
|||||||
return { service: "" };
|
return { service: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HealthCheckRequest = {
|
export const HealthCheckRequest: MessageFns<HealthCheckRequest> = {
|
||||||
encode(message: HealthCheckRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
encode(message: HealthCheckRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
if (message.service !== "") {
|
if (message.service !== "") {
|
||||||
writer.uint32(10).string(message.service);
|
writer.uint32(10).string(message.service);
|
||||||
}
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
|
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckRequest {
|
||||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseHealthCheckRequest();
|
const message = createBaseHealthCheckRequest();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
const tag = reader.uint32();
|
const tag = reader.uint32();
|
||||||
switch (tag >>> 3) {
|
switch (tag >>> 3) {
|
||||||
case 1:
|
case 1: {
|
||||||
if (tag !== 10) {
|
if (tag !== 10) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -97,28 +103,30 @@ export const HealthCheckRequest = {
|
|||||||
message.service = reader.string();
|
message.service = reader.string();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
reader.skipType(tag & 7);
|
reader.skip(tag & 7);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
|
||||||
fromJSON(object: any): HealthCheckRequest {
|
fromJSON(object: any): HealthCheckRequest {
|
||||||
return { service: isSet(object.service) ? String(object.service) : "" };
|
return { service: isSet(object.service) ? globalThis.String(object.service) : "" };
|
||||||
},
|
},
|
||||||
|
|
||||||
toJSON(message: HealthCheckRequest): unknown {
|
toJSON(message: HealthCheckRequest): unknown {
|
||||||
const obj: any = {};
|
const obj: any = {};
|
||||||
message.service !== undefined && (obj.service = message.service);
|
if (message.service !== "") {
|
||||||
|
obj.service = message.service;
|
||||||
|
}
|
||||||
return obj;
|
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 ?? {});
|
return HealthCheckRequest.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
|
|
||||||
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
|
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
|
||||||
const message = createBaseHealthCheckRequest();
|
const message = createBaseHealthCheckRequest();
|
||||||
message.service = object.service ?? "";
|
message.service = object.service ?? "";
|
||||||
@@ -130,22 +138,22 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
|
|||||||
return { status: 0 };
|
return { status: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HealthCheckResponse = {
|
export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
|
||||||
encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
encode(message: HealthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
if (message.status !== 0) {
|
if (message.status !== 0) {
|
||||||
writer.uint32(8).int32(message.status);
|
writer.uint32(8).int32(message.status);
|
||||||
}
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
|
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckResponse {
|
||||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
const message = createBaseHealthCheckResponse();
|
const message = createBaseHealthCheckResponse();
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
const tag = reader.uint32();
|
const tag = reader.uint32();
|
||||||
switch (tag >>> 3) {
|
switch (tag >>> 3) {
|
||||||
case 1:
|
case 1: {
|
||||||
if (tag !== 8) {
|
if (tag !== 8) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -153,10 +161,11 @@ export const HealthCheckResponse = {
|
|||||||
message.status = reader.int32() as any;
|
message.status = reader.int32() as any;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
reader.skipType(tag & 7);
|
reader.skip(tag & 7);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
@@ -167,14 +176,15 @@ export const HealthCheckResponse = {
|
|||||||
|
|
||||||
toJSON(message: HealthCheckResponse): unknown {
|
toJSON(message: HealthCheckResponse): unknown {
|
||||||
const obj: any = {};
|
const obj: any = {};
|
||||||
message.status !== undefined && (obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
|
if (message.status !== 0) {
|
||||||
|
obj.status = healthCheckResponse_ServingStatusToJSON(message.status);
|
||||||
|
}
|
||||||
return obj;
|
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 ?? {});
|
return HealthCheckResponse.fromPartial(base ?? ({} as any));
|
||||||
},
|
},
|
||||||
|
|
||||||
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
|
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
|
||||||
const message = createBaseHealthCheckResponse();
|
const message = createBaseHealthCheckResponse();
|
||||||
message.status = object.status ?? 0;
|
message.status = object.status ?? 0;
|
||||||
@@ -236,12 +246,14 @@ export interface HealthClient extends Client {
|
|||||||
export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as {
|
export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as {
|
||||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;
|
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;
|
||||||
service: typeof HealthService;
|
service: typeof HealthService;
|
||||||
|
serviceName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
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 globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
: Partial<T>;
|
: Partial<T>;
|
||||||
|
|
||||||
@@ -252,3 +264,12 @@ export type Exact<P, I extends P> = P extends Builtin ? P
|
|||||||
function isSet(value: any): boolean {
|
function isSet(value: any): boolean {
|
||||||
return value !== null && value !== undefined;
|
return value !== null && value !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||||
|
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import Logger from './loaders/logger';
|
|
||||||
import config from './config';
|
|
||||||
import { HealthClient } from './protos/health';
|
|
||||||
import { credentials } from '@grpc/grpc-js';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const client = new HealthClient(
|
|
||||||
`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(200).send({ code: 500, error: err });
|
|
||||||
}
|
|
||||||
return res.status(200).send({ code: 200, data: response });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app
|
|
||||||
.listen(config.publicPort, '0.0.0.0', async () => {
|
|
||||||
await require('./loaders/sentry').default({ expressApp: app });
|
|
||||||
await require('./loaders/db').default();
|
|
||||||
|
|
||||||
Logger.debug(`✌️ 公共服务启动成功!`);
|
|
||||||
console.debug(`✌️ 公共服务启动成功!`);
|
|
||||||
process.send?.('ready');
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
Logger.error(err);
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
+10
-10
@@ -10,41 +10,41 @@ const addCron = (
|
|||||||
callback: sendUnaryData<AddCronResponse>,
|
callback: sendUnaryData<AddCronResponse>,
|
||||||
) => {
|
) => {
|
||||||
for (const item of call.request.crons) {
|
for (const item of call.request.crons) {
|
||||||
const { id, schedule, command, extraSchedules, name } = item;
|
const { id, schedule, command, extra_schedules, name } = item;
|
||||||
if (scheduleStacks.has(id)) {
|
if (scheduleStacks.has(id)) {
|
||||||
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
'[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
schedule,
|
schedule,
|
||||||
command,
|
command,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (extraSchedules?.length) {
|
if (extra_schedules?.length) {
|
||||||
extraSchedules.forEach(x => {
|
extra_schedules.forEach((x) => {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
'[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
x.schedule,
|
x.schedule,
|
||||||
command,
|
command,
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleStacks.set(id, [
|
scheduleStacks.set(id, [
|
||||||
nodeSchedule.scheduleJob(id, schedule, async () => {
|
nodeSchedule.scheduleJob(id, schedule, async () => {
|
||||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||||
runCron(command, { name, schedule, extraSchedules });
|
runCron(command, item);
|
||||||
}),
|
}),
|
||||||
...(extraSchedules?.length
|
...(extra_schedules?.length
|
||||||
? extraSchedules.map((x) =>
|
? extra_schedules.map((x) =>
|
||||||
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
||||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||||
runCron(command, { name, schedule, extraSchedules });
|
runCron(command, item);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import EnvService from '../services/env';
|
||||||
|
import { sendUnaryData, ServerUnaryCall } from '@grpc/grpc-js';
|
||||||
|
import {
|
||||||
|
CreateEnvRequest,
|
||||||
|
CronItem,
|
||||||
|
DeleteEnvsRequest,
|
||||||
|
DisableEnvsRequest,
|
||||||
|
EnableEnvsRequest,
|
||||||
|
EnvItem,
|
||||||
|
EnvResponse,
|
||||||
|
EnvsResponse,
|
||||||
|
GetEnvByIdRequest,
|
||||||
|
GetEnvsRequest,
|
||||||
|
MoveEnvRequest,
|
||||||
|
Response,
|
||||||
|
SystemNotifyRequest,
|
||||||
|
UpdateEnvNamesRequest,
|
||||||
|
UpdateEnvRequest,
|
||||||
|
} from '../protos/api';
|
||||||
|
import LoggerInstance from '../loaders/logger';
|
||||||
|
import pick from 'lodash/pick';
|
||||||
|
import SystemService from '../services/system';
|
||||||
|
import CronService from '../services/cron';
|
||||||
|
import {
|
||||||
|
CronDetailRequest,
|
||||||
|
CronDetailResponse,
|
||||||
|
CreateCronRequest,
|
||||||
|
UpdateCronRequest,
|
||||||
|
DeleteCronsRequest,
|
||||||
|
CronResponse,
|
||||||
|
} from '../protos/api';
|
||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
|
||||||
|
Container.set('logger', LoggerInstance);
|
||||||
|
|
||||||
|
export const getEnvs = async (
|
||||||
|
call: ServerUnaryCall<GetEnvsRequest, EnvsResponse>,
|
||||||
|
callback: sendUnaryData<EnvsResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.envs(call.request.searchValue);
|
||||||
|
callback(null, {
|
||||||
|
code: 200,
|
||||||
|
data: data.map((x) => ({ ...x, remarks: x.remarks || '' })),
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(null, {
|
||||||
|
code: 500,
|
||||||
|
data: [],
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createEnv = async (
|
||||||
|
call: ServerUnaryCall<CreateEnvRequest, EnvsResponse>,
|
||||||
|
callback: sendUnaryData<EnvsResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.create(call.request.envs);
|
||||||
|
callback(null, { code: 200, data });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateEnv = async (
|
||||||
|
call: ServerUnaryCall<UpdateEnvRequest, EnvResponse>,
|
||||||
|
callback: sendUnaryData<EnvResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!call.request.env?.id) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
data: undefined,
|
||||||
|
message: 'id parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.update(
|
||||||
|
pick(call.request.env, ['id', 'name', 'value', 'remarks']) as EnvItem,
|
||||||
|
);
|
||||||
|
callback(null, { code: 200, data });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteEnvs = async (
|
||||||
|
call: ServerUnaryCall<DeleteEnvsRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!call.request.ids || call.request.ids.length === 0) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
message: 'ids parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
await envService.remove(call.request.ids);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moveEnv = async (
|
||||||
|
call: ServerUnaryCall<MoveEnvRequest, EnvResponse>,
|
||||||
|
callback: sendUnaryData<EnvResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!call.request.id) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
data: undefined,
|
||||||
|
message: 'id parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.move(call.request.id, {
|
||||||
|
fromIndex: call.request.fromIndex,
|
||||||
|
toIndex: call.request.toIndex,
|
||||||
|
});
|
||||||
|
callback(null, { code: 200, data });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const disableEnvs = async (
|
||||||
|
call: ServerUnaryCall<DisableEnvsRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!call.request.ids || call.request.ids.length === 0) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
message: 'ids parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
await envService.disabled(call.request.ids);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const enableEnvs = async (
|
||||||
|
call: ServerUnaryCall<EnableEnvsRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!call.request.ids || call.request.ids.length === 0) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
message: 'ids parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
await envService.enabled(call.request.ids);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateEnvNames = async (
|
||||||
|
call: ServerUnaryCall<UpdateEnvNamesRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!call.request.ids || call.request.ids.length === 0) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
message: 'ids parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
await envService.updateNames({
|
||||||
|
ids: call.request.ids,
|
||||||
|
name: call.request.name,
|
||||||
|
});
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEnvById = async (
|
||||||
|
call: ServerUnaryCall<GetEnvByIdRequest, EnvResponse>,
|
||||||
|
callback: sendUnaryData<EnvResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!call.request.id) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
data: undefined,
|
||||||
|
message: 'id parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.getDb({ id: call.request.id });
|
||||||
|
callback(null, {
|
||||||
|
code: 200,
|
||||||
|
data: { ...data, remarks: data.remarks || '' },
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const systemNotify = async (
|
||||||
|
call: ServerUnaryCall<SystemNotifyRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const data = await systemService.notify({
|
||||||
|
title: call.request.title,
|
||||||
|
content: call.request.content,
|
||||||
|
notificationInfo: call.request.notificationInfo as unknown as NotificationInfo,
|
||||||
|
});
|
||||||
|
callback(null, data);
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeCronData = (data: CronItem | null): CronItem | undefined => {
|
||||||
|
if (!data) return undefined;
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
sub_id: data.sub_id ?? undefined,
|
||||||
|
extra_schedules: data.extra_schedules ?? undefined,
|
||||||
|
pid: data.pid ?? undefined,
|
||||||
|
task_before: data.task_before ?? undefined,
|
||||||
|
task_after: data.task_after ?? undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCronDetail = async (
|
||||||
|
call: ServerUnaryCall<CronDetailRequest, CronDetailResponse>,
|
||||||
|
callback: sendUnaryData<CronDetailResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!call.request.log_path) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
data: undefined,
|
||||||
|
message: 'log_path is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = (await cronService.find({
|
||||||
|
log_path: call.request.log_path,
|
||||||
|
})) as CronItem;
|
||||||
|
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCron = async (
|
||||||
|
call: ServerUnaryCall<CreateCronRequest, CronResponse>,
|
||||||
|
callback: sendUnaryData<CronResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = (await cronService.create(call.request)) as CronItem;
|
||||||
|
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateCron = async (
|
||||||
|
call: ServerUnaryCall<UpdateCronRequest, CronResponse>,
|
||||||
|
callback: sendUnaryData<CronResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const { id, ...fields } = call.request;
|
||||||
|
|
||||||
|
const updateRequest = {
|
||||||
|
id,
|
||||||
|
...Object.entries(fields).reduce((acc: any, [key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
} as UpdateCronRequest;
|
||||||
|
|
||||||
|
const data = (await cronService.update(updateRequest)) as CronItem;
|
||||||
|
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteCrons = async (
|
||||||
|
call: ServerUnaryCall<DeleteCronsRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
await cronService.remove(call.request.ids);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,7 +10,7 @@ import config from '../config';
|
|||||||
|
|
||||||
class Client {
|
class Client {
|
||||||
private client = new CronClient(
|
private client = new CronClient(
|
||||||
`0.0.0.0:${config.cronPort}`,
|
`0.0.0.0:${config.grpcPort}`,
|
||||||
credentials.createInsecure(),
|
credentials.createInsecure(),
|
||||||
{ 'grpc.enable_http_proxy': 0 },
|
{ 'grpc.enable_http_proxy': 0 },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const delCron = (
|
|||||||
for (const id of call.request.ids) {
|
for (const id of call.request.ids) {
|
||||||
if (scheduleStacks.has(id)) {
|
if (scheduleStacks.has(id)) {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][取消定时任务], 任务ID: %s',
|
'[schedule][取消定时任务] 任务ID: %s',
|
||||||
id,
|
id,
|
||||||
);
|
);
|
||||||
scheduleStacks.get(id)?.forEach(x => x.cancel());
|
scheduleStacks.get(id)?.forEach(x => x.cancel());
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Server, ServerCredentials } from '@grpc/grpc-js';
|
|
||||||
import { CronService } from '../protos/cron';
|
|
||||||
import { addCron } from './addCron';
|
|
||||||
import { delCron } from './delCron';
|
|
||||||
import { HealthService } from '../protos/health';
|
|
||||||
import { check } from './health';
|
|
||||||
import config from '../config';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
|
|
||||||
const server = new Server({ 'grpc.enable_http_proxy': 0 });
|
|
||||||
server.addService(HealthService, { check });
|
|
||||||
server.addService(CronService, { addCron, delCron });
|
|
||||||
server.bindAsync(
|
|
||||||
`0.0.0.0:${config.cronPort}`,
|
|
||||||
ServerCredentials.createInsecure(),
|
|
||||||
(err, port) => {
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
server.start();
|
|
||||||
Logger.debug(`✌️ 定时服务启动成功!`);
|
|
||||||
console.debug(`✌️ 定时服务启动成功!`);
|
|
||||||
process.send?.('ready');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
+17
-5
@@ -3,7 +3,7 @@ import path, { join } from 'path';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { getFileContentByName } from '../config/util';
|
import { getFileContentByName } from '../config/util';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import got from 'got';
|
import { request } from 'undici';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ConfigService {
|
export default class ConfigService {
|
||||||
@@ -11,14 +11,26 @@ export default class ConfigService {
|
|||||||
|
|
||||||
public async getFile(filePath: string, res: Response) {
|
public async getFile(filePath: string, res: Response) {
|
||||||
let content = '';
|
let content = '';
|
||||||
if (config.blackFileList.includes(filePath) || !filePath) {
|
const avaliablePath = [config.rootPath, config.configPath].map((x) =>
|
||||||
res.send({ code: 403, message: '文件无法访问' });
|
path.resolve(x, filePath),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.blackFileList.includes(filePath) ||
|
||||||
|
avaliablePath.every(
|
||||||
|
(x) =>
|
||||||
|
!x.startsWith(config.scriptPath) && !x.startsWith(config.configPath),
|
||||||
|
) ||
|
||||||
|
!filePath
|
||||||
|
) {
|
||||||
|
return res.send({ code: 403, message: '文件无法访问' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filePath.startsWith('sample/')) {
|
if (filePath.startsWith('sample/')) {
|
||||||
const res = await got.get(
|
const res = await request(
|
||||||
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
|
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
|
||||||
);
|
);
|
||||||
content = res.body;
|
content = await res.body.text();
|
||||||
} else if (filePath.startsWith('data/scripts/')) {
|
} else if (filePath.startsWith('data/scripts/')) {
|
||||||
content = await getFileContentByName(join(config.rootPath, filePath));
|
content = await getFileContentByName(join(config.rootPath, filePath));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+128
-38
@@ -11,8 +11,9 @@ import {
|
|||||||
killTask,
|
killTask,
|
||||||
getUniqPath,
|
getUniqPath,
|
||||||
safeJSONParse,
|
safeJSONParse,
|
||||||
|
isDemoEnv,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
|
import { Op, where, col as colFn, FindOptions, fn, Order } from 'sequelize';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
||||||
import cronClient from '../schedule/client';
|
import cronClient from '../schedule/client';
|
||||||
@@ -21,6 +22,8 @@ import { spawn } from 'cross-spawn';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import pickBy from 'lodash/pickBy';
|
import pickBy from 'lodash/pickBy';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
import { ScheduleType } from '../interface/schedule';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
@@ -34,22 +37,40 @@ export default class CronService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isOnceSchedule(schedule?: string) {
|
||||||
|
return schedule?.startsWith(ScheduleType.ONCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isBootSchedule(schedule?: string) {
|
||||||
|
return schedule?.startsWith(ScheduleType.BOOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSpecialSchedule(schedule?: string) {
|
||||||
|
return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
|
||||||
|
}
|
||||||
|
|
||||||
public async create(payload: Crontab): Promise<Crontab> {
|
public async create(payload: Crontab): Promise<Crontab> {
|
||||||
const tab = new Crontab(payload);
|
const tab = new Crontab(payload);
|
||||||
tab.saved = false;
|
tab.saved = false;
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
if (this.isNodeCron(doc)) {
|
|
||||||
|
if (isDemoEnv()) {
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) {
|
||||||
await cronClient.addCron([
|
await cronClient.addCron([
|
||||||
{
|
{
|
||||||
name: doc.name || '',
|
name: doc.name || '',
|
||||||
id: String(doc.id),
|
id: String(doc.id),
|
||||||
schedule: doc.schedule!,
|
schedule: doc.schedule!,
|
||||||
command: this.makeCommand(doc),
|
command: this.makeCommand(doc),
|
||||||
extraSchedules: doc.extra_schedules || [],
|
extra_schedules: doc.extra_schedules || [],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
await this.set_crontab();
|
|
||||||
|
await this.setCrontab();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,29 +78,33 @@ export default class CronService {
|
|||||||
return await CrontabModel.create(payload, { returning: true });
|
return await CrontabModel.create(payload, { returning: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Crontab): Promise<Crontab> {
|
public async update(payload: Partial<Crontab>): Promise<Crontab> {
|
||||||
const doc = await this.getDb({ id: payload.id });
|
const doc = await this.getDb({ id: payload.id });
|
||||||
const tab = new Crontab({ ...doc, ...payload });
|
const tab = new Crontab({ ...doc, ...payload });
|
||||||
tab.saved = false;
|
tab.saved = false;
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
if (doc.isDisabled === 1) {
|
|
||||||
|
if (doc.isDisabled === 1 || isDemoEnv()) {
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isNodeCron(doc)) {
|
if (this.isNodeCron(doc)) {
|
||||||
await cronClient.delCron([String(doc.id)]);
|
await cronClient.delCron([String(doc.id)]);
|
||||||
}
|
}
|
||||||
if (this.isNodeCron(newDoc)) {
|
|
||||||
|
if (this.isNodeCron(newDoc) && !this.isSpecialSchedule(newDoc.schedule)) {
|
||||||
await cronClient.addCron([
|
await cronClient.addCron([
|
||||||
{
|
{
|
||||||
name: doc.name || '',
|
name: doc.name || '',
|
||||||
id: String(newDoc.id),
|
id: String(newDoc.id),
|
||||||
schedule: newDoc.schedule!,
|
schedule: newDoc.schedule!,
|
||||||
command: this.makeCommand(newDoc),
|
command: this.makeCommand(newDoc),
|
||||||
extraSchedules: newDoc.extra_schedules || [],
|
extra_schedules: newDoc.extra_schedules || [],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
await this.set_crontab();
|
|
||||||
|
await this.setCrontab();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +159,7 @@ export default class CronService {
|
|||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
await CrontabModel.destroy({ where: { id: ids } });
|
await CrontabModel.destroy({ where: { id: ids } });
|
||||||
await cronClient.delCron(ids.map(String));
|
await cronClient.delCron(ids.map(String));
|
||||||
await this.set_crontab();
|
await this.setCrontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pin(ids: number[]) {
|
public async pin(ids: number[]) {
|
||||||
@@ -178,8 +203,8 @@ export default class CronService {
|
|||||||
for (const col of viewQuery.filters) {
|
for (const col of viewQuery.filters) {
|
||||||
const { property, value, operation } = col;
|
const { property, value, operation } = col;
|
||||||
let q: any = {};
|
let q: any = {};
|
||||||
let operate2 = null;
|
let operate2: any = null;
|
||||||
let operate = null;
|
let operate: any = null;
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'Reg':
|
case 'Reg':
|
||||||
operate = Op.like;
|
operate = Op.like;
|
||||||
@@ -190,22 +215,39 @@ export default class CronService {
|
|||||||
operate2 = Op.and;
|
operate2 = Op.and;
|
||||||
break;
|
break;
|
||||||
case 'In':
|
case 'In':
|
||||||
|
if (
|
||||||
|
property === 'status' &&
|
||||||
|
!value.includes(CrontabStatus.disabled)
|
||||||
|
) {
|
||||||
|
q[Op.and] = [
|
||||||
|
{ [property]: Array.isArray(value) ? value : [value] },
|
||||||
|
{ isDisabled: 0 },
|
||||||
|
];
|
||||||
|
} else {
|
||||||
q[Op.or] = [
|
q[Op.or] = [
|
||||||
{
|
{
|
||||||
[property]: Array.isArray(value) ? value : [value],
|
[property]: Array.isArray(value) ? value : [value],
|
||||||
},
|
},
|
||||||
property === 'status' && value.includes(2)
|
property === 'status' && value.includes(CrontabStatus.disabled)
|
||||||
? { isDisabled: 1 }
|
? { isDisabled: 1 }
|
||||||
: {},
|
: {},
|
||||||
];
|
];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'Nin':
|
case 'Nin':
|
||||||
q[Op.and] = [
|
q[Op.and] = [
|
||||||
|
{
|
||||||
|
[Op.or]: [
|
||||||
{
|
{
|
||||||
[property]: {
|
[property]: {
|
||||||
[Op.notIn]: Array.isArray(value) ? value : [value],
|
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
[property]: { [Op.is]: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
property === 'status' && value.includes(2)
|
property === 'status' && value.includes(2)
|
||||||
? { isDisabled: { [Op.ne]: 1 } }
|
? { isDisabled: { [Op.ne]: 1 } }
|
||||||
: {},
|
: {},
|
||||||
@@ -319,10 +361,10 @@ export default class CronService {
|
|||||||
log_path,
|
log_path,
|
||||||
}: {
|
}: {
|
||||||
log_path: string;
|
log_path: string;
|
||||||
}): Promise<Crontab | null> {
|
}): Promise<Crontab | undefined> {
|
||||||
try {
|
try {
|
||||||
const result = await CrontabModel.findOne({ where: { log_path } });
|
const result = await CrontabModel.findOne({ where: { log_path } });
|
||||||
return result;
|
return result?.get({ plain: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -362,9 +404,9 @@ export default class CronService {
|
|||||||
order.unshift([field, type]);
|
order.unshift([field, type]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let condition: any = {
|
let condition: FindOptions<Crontab> = {
|
||||||
where: query,
|
where: query,
|
||||||
order: order,
|
order: order as Order,
|
||||||
};
|
};
|
||||||
if (page && size) {
|
if (page && size) {
|
||||||
condition.offset = (page - 1) * size;
|
condition.offset = (page - 1) * size;
|
||||||
@@ -373,7 +415,7 @@ export default class CronService {
|
|||||||
try {
|
try {
|
||||||
const result = await CrontabModel.findAll(condition);
|
const result = await CrontabModel.findAll(condition);
|
||||||
const count = await CrontabModel.count({ where: query });
|
const count = await CrontabModel.count({ where: query });
|
||||||
return { data: result, total: count };
|
return { data: result.map((x) => x.get({ plain: true })), total: count };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -423,7 +465,7 @@ export default class CronService {
|
|||||||
name: cron.name,
|
name: cron.name,
|
||||||
command: cron.command,
|
command: cron.command,
|
||||||
schedule: cron.schedule,
|
schedule: cron.schedule,
|
||||||
extraSchedules: cron.extra_schedules,
|
extra_schedules: cron.extra_schedules,
|
||||||
};
|
};
|
||||||
if (cron.status !== CrontabStatus.queued) {
|
if (cron.status !== CrontabStatus.queued) {
|
||||||
resolve(params);
|
resolve(params);
|
||||||
@@ -431,7 +473,7 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`,
|
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
let { id, command, log_path } = cron;
|
let { id, command, log_path } = cron;
|
||||||
@@ -459,13 +501,28 @@ export default class CronService {
|
|||||||
await fs.appendFile(absolutePath, data.toString());
|
await fs.appendFile(absolutePath, data.toString());
|
||||||
});
|
});
|
||||||
cp.stderr.on('data', async (data) => {
|
cp.stderr.on('data', async (data) => {
|
||||||
|
this.logger.info(
|
||||||
|
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
command,
|
||||||
|
data.toString(),
|
||||||
|
);
|
||||||
await fs.appendFile(absolutePath, data.toString());
|
await fs.appendFile(absolutePath, data.toString());
|
||||||
});
|
});
|
||||||
cp.on('error', async (err) => {
|
cp.on('error', async (err) => {
|
||||||
|
this.logger.error(
|
||||||
|
'[panel][创建任务失败] 命令: %s, 错误信息: %j',
|
||||||
|
command,
|
||||||
|
err,
|
||||||
|
);
|
||||||
await fs.appendFile(absolutePath, JSON.stringify(err));
|
await fs.appendFile(absolutePath, JSON.stringify(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
cp.on('exit', async (code) => {
|
cp.on('exit', async (code) => {
|
||||||
|
this.logger.info(
|
||||||
|
'[panel][执行任务结束] 参数: %s, 退出码: %j',
|
||||||
|
JSON.stringify(params),
|
||||||
|
code,
|
||||||
|
);
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.idle, pid: undefined },
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
{ where: { id } },
|
{ where: { id } },
|
||||||
@@ -479,23 +536,27 @@ export default class CronService {
|
|||||||
public async disabled(ids: number[]) {
|
public async disabled(ids: number[]) {
|
||||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||||
await cronClient.delCron(ids.map(String));
|
await cronClient.delCron(ids.map(String));
|
||||||
await this.set_crontab();
|
await this.setCrontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
public async enabled(ids: number[]) {
|
||||||
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||||
const sixCron = docs
|
const sixCron = docs
|
||||||
.filter((x) => this.isNodeCron(x))
|
.filter((x) => this.isNodeCron(x) && !this.isSpecialSchedule(x.schedule))
|
||||||
.map((doc) => ({
|
.map((doc) => ({
|
||||||
name: doc.name || '',
|
name: doc.name || '',
|
||||||
id: String(doc.id),
|
id: String(doc.id),
|
||||||
schedule: doc.schedule!,
|
schedule: doc.schedule!,
|
||||||
command: this.makeCommand(doc),
|
command: this.makeCommand(doc),
|
||||||
extraSchedules: doc.extra_schedules || [],
|
extra_schedules: doc.extra_schedules || [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (isDemoEnv()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await cronClient.addCron(sixCron);
|
await cronClient.addCron(sixCron);
|
||||||
await this.set_crontab();
|
await this.setCrontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(id: number) {
|
public async log(id: number) {
|
||||||
@@ -509,7 +570,10 @@ export default class CronService {
|
|||||||
if (logFileExist) {
|
if (logFileExist) {
|
||||||
return await getFileContentByName(`${absolutePath}`);
|
return await getFileContentByName(`${absolutePath}`);
|
||||||
} else {
|
} else {
|
||||||
return '任务未运行';
|
return typeof doc.status === 'number' &&
|
||||||
|
[CrontabStatus.queued, CrontabStatus.running].includes(doc.status)
|
||||||
|
? '运行中...'
|
||||||
|
: '日志不存在...';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +593,7 @@ export default class CronService {
|
|||||||
files.map(async (x) => ({
|
files.map(async (x) => ({
|
||||||
filename: x,
|
filename: x,
|
||||||
directory: relativeDir.replace(config.logPath, ''),
|
directory: relativeDir.replace(config.logPath, ''),
|
||||||
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(),
|
time: (await fs.lstat(`${dir}/${x}`)).birthtimeMs,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
).sort((a, b) => b.time - a.time);
|
).sort((a, b) => b.time - a.time);
|
||||||
@@ -549,11 +613,13 @@ export default class CronService {
|
|||||||
if (tab.task_before) {
|
if (tab.task_before) {
|
||||||
commandVariable += `task_before='${tab.task_before
|
commandVariable += `task_before='${tab.task_before
|
||||||
.replace(/'/g, "'\\''")
|
.replace(/'/g, "'\\''")
|
||||||
|
.replace(/;? *\n/g, ';')
|
||||||
.trim()}' `;
|
.trim()}' `;
|
||||||
}
|
}
|
||||||
if (tab.task_after) {
|
if (tab.task_after) {
|
||||||
commandVariable += `task_after='${tab.task_after
|
commandVariable += `task_after='${tab.task_after
|
||||||
.replace(/'/g, "'\\''")
|
.replace(/'/g, "'\\''")
|
||||||
|
.replace(/;? *\n/g, ';')
|
||||||
.trim()}' `;
|
.trim()}' `;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,15 +627,14 @@ export default class CronService {
|
|||||||
return crontab_job_string;
|
return crontab_job_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async set_crontab(data?: { data: Crontab[]; total: number }) {
|
private async setCrontab(data?: { data: Crontab[]; total: number }) {
|
||||||
const tabs = data ?? (await this.crontabs());
|
const tabs = data ?? (await this.crontabs());
|
||||||
var crontab_string = '';
|
var crontab_string = '';
|
||||||
tabs.data.forEach((tab) => {
|
tabs.data.forEach((tab) => {
|
||||||
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
|
||||||
if (
|
if (
|
||||||
tab.isDisabled === 1 ||
|
tab.isDisabled === 1 ||
|
||||||
_schedule!.length !== 5 ||
|
this.isNodeCron(tab) ||
|
||||||
tab.extra_schedules?.length
|
this.isSpecialSchedule(tab.schedule)
|
||||||
) {
|
) {
|
||||||
crontab_string += '# ';
|
crontab_string += '# ';
|
||||||
crontab_string += tab.schedule;
|
crontab_string += tab.schedule;
|
||||||
@@ -584,13 +649,13 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await fs.writeFile(config.crontabFile, crontab_string);
|
await writeFileWithLock(config.crontabFile, crontab_string);
|
||||||
|
|
||||||
execSync(`crontab ${config.crontabFile}`);
|
execSync(`crontab ${config.crontabFile}`);
|
||||||
await CrontabModel.update({ saved: true }, { where: {} });
|
await CrontabModel.update({ saved: true }, { where: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
public import_crontab() {
|
public importCrontab() {
|
||||||
exec('crontab -l', (error, stdout, stderr) => {
|
exec('crontab -l', (error, stdout, stderr) => {
|
||||||
const lines = stdout.split('\n');
|
const lines = stdout.split('\n');
|
||||||
const namePrefix = new Date().getTime();
|
const namePrefix = new Date().getTime();
|
||||||
@@ -626,17 +691,42 @@ export default class CronService {
|
|||||||
|
|
||||||
public async autosave_crontab() {
|
public async autosave_crontab() {
|
||||||
const tabs = await this.crontabs();
|
const tabs = await this.crontabs();
|
||||||
this.set_crontab(tabs);
|
const regularCrons = tabs.data
|
||||||
|
.filter(
|
||||||
const sixCron = tabs.data
|
(x) =>
|
||||||
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
|
x.isDisabled !== 1 &&
|
||||||
|
this.isNodeCron(x) &&
|
||||||
|
!this.isSpecialSchedule(x.schedule),
|
||||||
|
)
|
||||||
.map((doc) => ({
|
.map((doc) => ({
|
||||||
name: doc.name || '',
|
name: doc.name || '',
|
||||||
id: String(doc.id),
|
id: String(doc.id),
|
||||||
schedule: doc.schedule!,
|
schedule: doc.schedule!,
|
||||||
command: this.makeCommand(doc),
|
command: this.makeCommand(doc),
|
||||||
extraSchedules: doc.extra_schedules || [],
|
extra_schedules: doc.extra_schedules || [],
|
||||||
}));
|
}));
|
||||||
await cronClient.addCron(sixCron);
|
|
||||||
|
if (isDemoEnv()) {
|
||||||
|
await writeFileWithLock(config.crontabFile, '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await cronClient.addCron(regularCrons);
|
||||||
|
this.setCrontab(tabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async bootTask() {
|
||||||
|
const tabs = await this.crontabs();
|
||||||
|
const bootTasks = tabs.data.filter(
|
||||||
|
(x) => !x.isDisabled && this.isBootSchedule(x.schedule),
|
||||||
|
);
|
||||||
|
if (bootTasks.length > 0) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.queued },
|
||||||
|
{ where: { id: bootTasks.map((t) => t.id!) } },
|
||||||
|
);
|
||||||
|
for (const task of bootTasks) {
|
||||||
|
await this.runSingle(task.id!);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-42
@@ -3,12 +3,9 @@ import winston from 'winston';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import {
|
import {
|
||||||
Dependence,
|
Dependence,
|
||||||
InstallDependenceCommandTypes,
|
|
||||||
DependenceStatus,
|
DependenceStatus,
|
||||||
DependenceTypes,
|
DependenceTypes,
|
||||||
unInstallDependenceCommandTypes,
|
|
||||||
DependenceModel,
|
DependenceModel,
|
||||||
GetDependenceCommandTypes,
|
|
||||||
versionDependenceCommandTypes,
|
versionDependenceCommandTypes,
|
||||||
} from '../data/dependence';
|
} from '../data/dependence';
|
||||||
import { spawn } from 'cross-spawn';
|
import { spawn } from 'cross-spawn';
|
||||||
@@ -19,6 +16,9 @@ import {
|
|||||||
getPid,
|
getPid,
|
||||||
killTask,
|
killTask,
|
||||||
promiseExecSuccess,
|
promiseExecSuccess,
|
||||||
|
getInstallCommand,
|
||||||
|
getUninstallCommand,
|
||||||
|
getGetCommand,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import taskLimit from '../shared/pLimit';
|
import taskLimit from '../shared/pLimit';
|
||||||
@@ -67,6 +67,9 @@ export default class DependenceService {
|
|||||||
|
|
||||||
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
taskLimit.removeQueuedDependency(doc);
|
||||||
|
}
|
||||||
const unInstalledDeps = docs.filter(
|
const unInstalledDeps = docs.filter(
|
||||||
(x) => x.status !== DependenceStatus.installed,
|
(x) => x.status !== DependenceStatus.installed,
|
||||||
);
|
);
|
||||||
@@ -95,34 +98,32 @@ export default class DependenceService {
|
|||||||
searchValue,
|
searchValue,
|
||||||
type,
|
type,
|
||||||
status,
|
status,
|
||||||
}: { searchValue: string; type: string; status: string },
|
}: {
|
||||||
|
searchValue: string;
|
||||||
|
type: keyof typeof DependenceTypes;
|
||||||
|
status: string;
|
||||||
|
},
|
||||||
sort: any = [],
|
sort: any = [],
|
||||||
query: any = {},
|
query: any = {},
|
||||||
): Promise<Dependence[]> {
|
): Promise<Dependence[]> {
|
||||||
let condition = {
|
let condition = query;
|
||||||
...query,
|
if (DependenceTypes[type]) {
|
||||||
type: DependenceTypes[type as any],
|
condition.type = DependenceTypes[type];
|
||||||
};
|
}
|
||||||
if (status) {
|
if (status) {
|
||||||
condition.status = status.split(',').map(Number);
|
condition.status = status.split(',').map(Number);
|
||||||
}
|
}
|
||||||
if (searchValue) {
|
if (searchValue) {
|
||||||
const encodeText = encodeURI(searchValue);
|
const encodeText = encodeURI(searchValue);
|
||||||
const reg = {
|
condition.name = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ [Op.like]: `%${searchValue}%` },
|
{ [Op.like]: `%${searchValue}%` },
|
||||||
{ [Op.like]: `%${encodeText}%` },
|
{ [Op.like]: `%${encodeText}%` },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
condition = {
|
|
||||||
...condition,
|
|
||||||
name: reg,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition, sort);
|
return await this.find(condition, sort);
|
||||||
return result as any;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -132,10 +133,12 @@ export default class DependenceService {
|
|||||||
docs: Dependence[],
|
docs: Dependence[],
|
||||||
isInstall: boolean = true,
|
isInstall: boolean = true,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
) {
|
): Promise<void> {
|
||||||
docs.forEach((dep) => {
|
docs.forEach((dep) => {
|
||||||
this.installOrUninstallDependency(dep, isInstall, force);
|
this.installOrUninstallDependency(dep, isInstall, force);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return taskLimit.waitDependencyQueueDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
||||||
@@ -145,6 +148,9 @@ export default class DependenceService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
|
for (const doc of docs) {
|
||||||
|
taskLimit.removeQueuedDependency(doc);
|
||||||
|
}
|
||||||
this.installDependenceOneByOne(docs, true, true);
|
this.installDependenceOneByOne(docs, true, true);
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
@@ -153,13 +159,11 @@ export default class DependenceService {
|
|||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
taskLimit.removeQueuedDependency(doc);
|
taskLimit.removeQueuedDependency(doc);
|
||||||
const depInstallCommand = InstallDependenceCommandTypes[doc.type];
|
const depInstallCommand = getInstallCommand(doc.type, doc.name);
|
||||||
const depUnInstallCommand = unInstallDependenceCommandTypes[doc.type];
|
const depUnInstallCommand = getUninstallCommand(doc.type, doc.name);
|
||||||
const installCmd = `${depInstallCommand} ${doc.name.trim()}`;
|
|
||||||
const unInstallCmd = `${depUnInstallCommand} ${doc.name.trim()}`;
|
|
||||||
const pids = await Promise.all([
|
const pids = await Promise.all([
|
||||||
getPid(installCmd),
|
getPid(depInstallCommand),
|
||||||
getPid(unInstallCmd),
|
getPid(depUnInstallCommand),
|
||||||
]);
|
]);
|
||||||
for (const pid of pids) {
|
for (const pid of pids) {
|
||||||
pid && (await killTask(pid));
|
pid && (await killTask(pid));
|
||||||
@@ -226,11 +230,9 @@ export default class DependenceService {
|
|||||||
? 'installDependence'
|
? 'installDependence'
|
||||||
: 'uninstallDependence';
|
: 'uninstallDependence';
|
||||||
let depName = dependency.name.trim();
|
let depName = dependency.name.trim();
|
||||||
const depRunCommand = (
|
const command = isInstall
|
||||||
isInstall
|
? getInstallCommand(dependency.type, depName)
|
||||||
? InstallDependenceCommandTypes
|
: getUninstallCommand(dependency.type, depName);
|
||||||
: unInstallDependenceCommandTypes
|
|
||||||
)[dependency.type];
|
|
||||||
const actionText = isInstall ? '安装' : '删除';
|
const actionText = isInstall ? '安装' : '删除';
|
||||||
const startTime = dayjs();
|
const startTime = dayjs();
|
||||||
|
|
||||||
@@ -246,7 +248,7 @@ export default class DependenceService {
|
|||||||
|
|
||||||
// 判断是否已经安装过依赖
|
// 判断是否已经安装过依赖
|
||||||
if (isInstall && !force) {
|
if (isInstall && !force) {
|
||||||
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
|
const getCommand = getGetCommand(dependency.type, depName);
|
||||||
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
||||||
let depVersion = '';
|
let depVersion = '';
|
||||||
if (depName.includes(depVersionStr)) {
|
if (depName.includes(depVersionStr)) {
|
||||||
@@ -263,13 +265,7 @@ export default class DependenceService {
|
|||||||
const isLinuxDependence = dependency.type === DependenceTypes.linux;
|
const isLinuxDependence = dependency.type === DependenceTypes.linux;
|
||||||
const isPythonDependence =
|
const isPythonDependence =
|
||||||
dependency.type === DependenceTypes.python3;
|
dependency.type === DependenceTypes.python3;
|
||||||
const depInfo = (
|
const depInfo = (await promiseExecSuccess(getCommand))
|
||||||
await promiseExecSuccess(
|
|
||||||
isNodeDependence
|
|
||||||
? `${getCommandPrefix} | grep "${depName}" | head -1`
|
|
||||||
: `${getCommandPrefix} ${depName}`,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.replace(/\s{2,}/, ' ')
|
.replace(/\s{2,}/, ' ')
|
||||||
.replace(/\s+$/, '');
|
.replace(/\s+$/, '');
|
||||||
|
|
||||||
@@ -304,12 +300,9 @@ export default class DependenceService {
|
|||||||
const proxyStr = dependenceProxyFileExist
|
const proxyStr = dependenceProxyFileExist
|
||||||
? `source ${config.dependenceProxyFile} &&`
|
? `source ${config.dependenceProxyFile} &&`
|
||||||
: '';
|
: '';
|
||||||
const cp = spawn(
|
const cp = spawn(`${proxyStr} ${command}`, {
|
||||||
`${proxyStr} ${depRunCommand} ${dependency.name.trim()}`,
|
|
||||||
{
|
|
||||||
shell: '/bin/bash',
|
shell: '/bin/bash',
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
cp.stdout.on('data', async (data) => {
|
cp.stdout.on('data', async (data) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
@@ -353,7 +346,7 @@ export default class DependenceService {
|
|||||||
});
|
});
|
||||||
this.updateLog(depIds, message);
|
this.updateLog(depIds, message);
|
||||||
|
|
||||||
let status = null;
|
let status: number;
|
||||||
if (isSucceed) {
|
if (isSucceed) {
|
||||||
status = isInstall
|
status = isInstall
|
||||||
? DependenceStatus.installed
|
? DependenceStatus.installed
|
||||||
|
|||||||
+11
-10
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '../data/env';
|
} from '../data/env';
|
||||||
import groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
import { FindOptions, Op } from 'sequelize';
|
import { FindOptions, Op } from 'sequelize';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class EnvService {
|
export default class EnvService {
|
||||||
@@ -40,7 +41,7 @@ export default class EnvService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payloads: Env[]): Promise<Env[]> {
|
public async insert(payloads: Env[]): Promise<Env[]> {
|
||||||
const result = [];
|
const result: Env[] = [];
|
||||||
for (const env of payloads) {
|
for (const env of payloads) {
|
||||||
const doc = await EnvModel.create(env, { returning: true });
|
const doc = await EnvModel.create(env, { returning: true });
|
||||||
result.push(doc);
|
result.push(doc);
|
||||||
@@ -61,7 +62,7 @@ export default class EnvService {
|
|||||||
return await this.getDb({ id: payload.id });
|
return await this.getDb({ id: payload.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: string[]) {
|
public async remove(ids: number[]) {
|
||||||
await EnvModel.destroy({ where: { id: ids } });
|
await EnvModel.destroy({ where: { id: ids } });
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,7 @@ export default class EnvService {
|
|||||||
['position', 'DESC'],
|
['position', 'DESC'],
|
||||||
['createdAt', 'ASC'],
|
['createdAt', 'ASC'],
|
||||||
]);
|
]);
|
||||||
return result as any;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -160,7 +161,7 @@ export default class EnvService {
|
|||||||
where: { ...query },
|
where: { ...query },
|
||||||
order: [...sort],
|
order: [...sort],
|
||||||
});
|
});
|
||||||
return docs;
|
return docs.map((x) => x.get({ plain: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
||||||
@@ -171,7 +172,7 @@ export default class EnvService {
|
|||||||
return doc.get({ plain: true });
|
return doc.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: string[]) {
|
public async disabled(ids: number[]) {
|
||||||
await EnvModel.update(
|
await EnvModel.update(
|
||||||
{ status: EnvStatus.disabled },
|
{ status: EnvStatus.disabled },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
@@ -179,12 +180,12 @@ export default class EnvService {
|
|||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: string[]) {
|
public async enabled(ids: number[]) {
|
||||||
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
public async updateNames({ ids, name }: { ids: number[]; name: string }) {
|
||||||
await EnvModel.update({ name }, { where: { id: ids } });
|
await EnvModel.update({ name }, { where: { id: ids } });
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
@@ -225,8 +226,8 @@ export default class EnvService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await fs.writeFile(config.envFile, env_string);
|
await writeFileWithLock(config.envFile, env_string);
|
||||||
await fs.writeFile(config.jsEnvFile, js_env_string);
|
await writeFileWithLock(config.jsEnvFile, js_env_string);
|
||||||
await fs.writeFile(config.pyEnvFile, py_env_string);
|
await writeFileWithLock(config.pyEnvFile, py_env_string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Server, ServerCredentials } from '@grpc/grpc-js';
|
||||||
|
import { CronService } from '../protos/cron';
|
||||||
|
import { HealthService } from '../protos/health';
|
||||||
|
import { ApiService } from '../protos/api';
|
||||||
|
import { addCron } from '../schedule/addCron';
|
||||||
|
import { delCron } from '../schedule/delCron';
|
||||||
|
import { check } from '../schedule/health';
|
||||||
|
import * as Api from '../schedule/api';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import config from '../config';
|
||||||
|
import { metricsService } from './metrics';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GrpcServerService {
|
||||||
|
private server: Server = new Server({ 'grpc.enable_http_proxy': 0 });
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
try {
|
||||||
|
this.server.addService(HealthService, { check });
|
||||||
|
this.server.addService(CronService, { addCron, delCron });
|
||||||
|
this.server.addService(ApiService, Api);
|
||||||
|
|
||||||
|
const grpcPort = config.grpcPort;
|
||||||
|
const bindAsync = promisify(this.server.bindAsync).bind(this.server);
|
||||||
|
await bindAsync(
|
||||||
|
`0.0.0.0:${grpcPort}`,
|
||||||
|
ServerCredentials.createInsecure(),
|
||||||
|
);
|
||||||
|
Logger.debug(`✌️ gRPC service started successfully`);
|
||||||
|
|
||||||
|
metricsService.record('grpc_service_start', 1, {
|
||||||
|
port: grpcPort.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return grpcPort;
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error('Failed to start gRPC service:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async shutdown() {
|
||||||
|
try {
|
||||||
|
if (this.server) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
this.server.tryShutdown(() => {
|
||||||
|
Logger.debug('gRPC service stopped');
|
||||||
|
metricsService.record('grpc_service_stop', 1);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error('Error while shutting down gRPC service:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getServer() {
|
||||||
|
return this.server;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import { GrpcServerService } from './grpc';
|
||||||
|
import { HttpServerService } from './http';
|
||||||
|
|
||||||
|
interface HealthStatus {
|
||||||
|
status: 'ok' | 'error';
|
||||||
|
services: {
|
||||||
|
http: boolean;
|
||||||
|
grpc: boolean;
|
||||||
|
};
|
||||||
|
metrics: {
|
||||||
|
uptime: number;
|
||||||
|
memory: {
|
||||||
|
used: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class HealthService {
|
||||||
|
private startTime = Date.now();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private grpcServerService: GrpcServerService,
|
||||||
|
private httpServerService: HttpServerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async check(): Promise<HealthStatus> {
|
||||||
|
const status: HealthStatus = {
|
||||||
|
status: 'ok',
|
||||||
|
services: {
|
||||||
|
http: true,
|
||||||
|
grpc: true,
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
||||||
|
memory: {
|
||||||
|
used: process.memoryUsage().heapUsed,
|
||||||
|
total: process.memoryUsage().heapTotal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const httpServer = this.httpServerService.getServer();
|
||||||
|
if (!httpServer) {
|
||||||
|
status.services.http = false;
|
||||||
|
status.status = 'error';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
status.services.http = false;
|
||||||
|
status.status = 'error';
|
||||||
|
Logger.error('HTTP server check failed:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const grpcServer = this.grpcServerService.getServer();
|
||||||
|
if (!grpcServer) {
|
||||||
|
status.services.grpc = false;
|
||||||
|
status.status = 'error';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
status.services.grpc = false;
|
||||||
|
status.status = 'error';
|
||||||
|
Logger.error('gRPC server check failed:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
import { metricsService } from './metrics';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
import { Server } from 'http';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class HttpServerService {
|
||||||
|
private server?: Server = undefined;
|
||||||
|
|
||||||
|
async initialize(expressApp: express.Application, port: number) {
|
||||||
|
try {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server = expressApp.listen(port, '0.0.0.0', () => {
|
||||||
|
Logger.debug(`✌️ HTTP service started successfully`);
|
||||||
|
metricsService.record('http_service_start', 1, {
|
||||||
|
port: port.toString(),
|
||||||
|
});
|
||||||
|
resolve(this.server);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server?.on('error', (err: Error) => {
|
||||||
|
Logger.error('Failed to start HTTP service:', err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error('Failed to start HTTP service:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async shutdown() {
|
||||||
|
try {
|
||||||
|
if (this.server) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
this.server?.close(() => {
|
||||||
|
Logger.debug('HTTP service stopped');
|
||||||
|
metricsService.record('http_service_stop', 1);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error('Error while shutting down HTTP service:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getServer() {
|
||||||
|
return this.server;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class LogService {
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public checkFilePath(filePath: string, fileName: string) {
|
||||||
|
const finalPath = path.resolve(config.logPath, filePath, fileName);
|
||||||
|
return finalPath.startsWith(config.logPath) ? finalPath : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { performance } from 'perf_hooks';
|
||||||
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
|
interface Metric {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
timestamp: number;
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetricsService {
|
||||||
|
private metrics: Metric[] = [];
|
||||||
|
private static instance: MetricsService;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
// 定期清理旧数据
|
||||||
|
setInterval(() => {
|
||||||
|
const oneHourAgo = Date.now() - 3600000;
|
||||||
|
this.metrics = this.metrics.filter(m => m.timestamp > oneHourAgo);
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): MetricsService {
|
||||||
|
if (!MetricsService.instance) {
|
||||||
|
MetricsService.instance = new MetricsService();
|
||||||
|
}
|
||||||
|
return MetricsService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
record(name: string, value: number, tags?: Record<string, string>) {
|
||||||
|
this.metrics.push({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
tags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
measure(name: string, fn: () => void, tags?: Record<string, string>) {
|
||||||
|
const start = performance.now();
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} finally {
|
||||||
|
const duration = performance.now() - start;
|
||||||
|
this.record(name, duration, tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async measureAsync(name: string, fn: () => Promise<void>, tags?: Record<string, string>) {
|
||||||
|
const start = performance.now();
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
} finally {
|
||||||
|
const duration = performance.now() - start;
|
||||||
|
this.record(name, duration, tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetrics(name?: string, tags?: Record<string, string>) {
|
||||||
|
let filtered = this.metrics;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
filtered = filtered.filter(m => m.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
filtered = filtered.filter(m => {
|
||||||
|
if (!m.tags) return false;
|
||||||
|
return Object.entries(tags).every(([key, value]) => m.tags![key] === value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: filtered.length,
|
||||||
|
average: filtered.reduce((acc, curr) => acc + curr.value, 0) / filtered.length,
|
||||||
|
min: Math.min(...filtered.map(m => m.value)),
|
||||||
|
max: Math.max(...filtered.map(m => m.value)),
|
||||||
|
metrics: filtered,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
report() {
|
||||||
|
const report = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
metrics: this.getMetrics(),
|
||||||
|
};
|
||||||
|
Logger.info('性能指标报告:', report);
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metricsService = MetricsService.getInstance();
|
||||||
+199
-103
@@ -1,12 +1,11 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import got from 'got';
|
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import winston from 'winston';
|
|
||||||
import { parseBody, parseHeaders } from '../config/util';
|
import { parseBody, parseHeaders } from '../config/util';
|
||||||
import { NotificationInfo } from '../data/notify';
|
import { NotificationInfo } from '../data/notify';
|
||||||
import UserService from './user';
|
import UserService from './user';
|
||||||
|
import { httpClient } from '../config/http';
|
||||||
|
import { ProxyAgent } from 'undici';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class NotificationService {
|
export default class NotificationService {
|
||||||
@@ -33,6 +32,8 @@ export default class NotificationService {
|
|||||||
['webhook', this.webhook],
|
['webhook', this.webhook],
|
||||||
['lark', this.lark],
|
['lark', this.lark],
|
||||||
['chronocat', this.chronocat],
|
['chronocat', this.chronocat],
|
||||||
|
['ntfy', this.ntfy],
|
||||||
|
['wxPusherBot', this.wxPusherBot],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
private title = '';
|
private title = '';
|
||||||
@@ -43,22 +44,31 @@ export default class NotificationService {
|
|||||||
retry: 1,
|
retry: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor() {}
|
||||||
|
|
||||||
public async notify(
|
public async notify(
|
||||||
title: string,
|
title: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
notificationInfo?: NotificationInfo,
|
||||||
): Promise<boolean | undefined> {
|
): Promise<boolean | undefined> {
|
||||||
const { type, ...rest } = await this.userService.getNotificationMode();
|
let { type, ...rest } = await this.userService.getNotificationMode();
|
||||||
|
if (notificationInfo?.type) {
|
||||||
|
type = notificationInfo?.type;
|
||||||
|
}
|
||||||
if (type) {
|
if (type) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.params = rest;
|
let params = rest;
|
||||||
|
if (notificationInfo) {
|
||||||
|
const { type: _, ...others } = notificationInfo;
|
||||||
|
params = { ...rest, ...others };
|
||||||
|
}
|
||||||
|
this.params = params;
|
||||||
const notificationModeAction = this.modeMap.get(type);
|
const notificationModeAction = this.modeMap.get(type);
|
||||||
try {
|
try {
|
||||||
return await notificationModeAction?.call(this);
|
return await notificationModeAction?.call(this);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw error;
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -83,8 +93,9 @@ export default class NotificationService {
|
|||||||
private async gotify() {
|
private async gotify() {
|
||||||
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
|
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(
|
||||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
`${gotifyUrl}/message?token=${gotifyToken}`,
|
||||||
|
{
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
body: `title=${encodeURIComponent(
|
body: `title=${encodeURIComponent(
|
||||||
this.title,
|
this.title,
|
||||||
@@ -94,8 +105,8 @@ export default class NotificationService {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
.json();
|
);
|
||||||
if (typeof res.id === 'number') {
|
if (typeof res.id === 'number') {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -109,13 +120,11 @@ export default class NotificationService {
|
|||||||
private async goCqHttpBot() {
|
private async goCqHttpBot() {
|
||||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||||
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: { message: `${this.title}\n${this.content}` },
|
json: { message: `${this.title}\n${this.content}` },
|
||||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (res.retcode === 0) {
|
if (res.retcode === 0) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -128,17 +137,20 @@ export default class NotificationService {
|
|||||||
|
|
||||||
private async serverChan() {
|
private async serverChan() {
|
||||||
const { serverChanKey } = this.params;
|
const { serverChanKey } = this.params;
|
||||||
const url = serverChanKey.startsWith('SCT')
|
const matchResult = serverChanKey.match(/^sctp(\d+)t/i);
|
||||||
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
const url =
|
||||||
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
matchResult && matchResult[1]
|
||||||
|
? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send`
|
||||||
|
: `https://sctapi.ftqq.com/${serverChanKey}.send`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
body: `title=${this.title}&desp=${this.content}`,
|
body: `title=${encodeURIComponent(
|
||||||
|
this.title,
|
||||||
|
)}&desp=${encodeURIComponent(this.content)}`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (res.errno === 0 || res.data.errno === 0) {
|
if (res.errno === 0 || res.data.errno === 0) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -153,15 +165,13 @@ export default class NotificationService {
|
|||||||
const { pushDeerKey, pushDeerUrl } = this.params;
|
const { pushDeerKey, pushDeerUrl } = this.params;
|
||||||
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
||||||
this.title,
|
this.title,
|
||||||
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (
|
if (
|
||||||
res.content.result.length !== undefined &&
|
res.content.result.length !== undefined &&
|
||||||
res.content.result.length > 0
|
res.content.result.length > 0
|
||||||
@@ -176,16 +186,13 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async chat() {
|
private async chat() {
|
||||||
const { chatUrl, chatToken } = this.params;
|
const { synologyChatUrl } = this.params;
|
||||||
const url = `${chatUrl}${chatToken}`;
|
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(synologyChatUrl, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -221,13 +228,11 @@ export default class NotificationService {
|
|||||||
url: barkUrl,
|
url: barkUrl,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: body,
|
json: body,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -253,29 +258,17 @@ export default class NotificationService {
|
|||||||
}/bot${telegramBotToken}/sendMessage`;
|
}/bot${telegramBotToken}/sendMessage`;
|
||||||
let agent;
|
let agent;
|
||||||
if (telegramBotProxyHost && telegramBotProxyPort) {
|
if (telegramBotProxyHost && telegramBotProxyPort) {
|
||||||
const options: any = {
|
agent = new ProxyAgent({
|
||||||
keepAlive: true,
|
uri: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
|
||||||
keepAliveMsecs: 1000,
|
});
|
||||||
maxSockets: 256,
|
|
||||||
maxFreeSockets: 256,
|
|
||||||
proxy: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
|
|
||||||
};
|
|
||||||
const httpAgent = new HttpProxyAgent(options);
|
|
||||||
const httpsAgent = new HttpsProxyAgent(options);
|
|
||||||
agent = {
|
|
||||||
http: httpAgent,
|
|
||||||
https: httpsAgent,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
agent,
|
dispatcher: agent,
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -298,8 +291,7 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
@@ -307,8 +299,7 @@ export default class NotificationService {
|
|||||||
content: ` ${this.title}\n\n${this.content}`,
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (res.errcode === 0) {
|
if (res.errcode === 0) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -324,8 +315,7 @@ export default class NotificationService {
|
|||||||
this.params;
|
this.params;
|
||||||
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
@@ -333,8 +323,7 @@ export default class NotificationService {
|
|||||||
content: ` ${this.title}\n\n${this.content}`,
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (res.errcode === 0) {
|
if (res.errcode === 0) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -351,15 +340,13 @@ export default class NotificationService {
|
|||||||
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||||
weWorkAppKey.split(',');
|
weWorkAppKey.split(',');
|
||||||
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
|
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
|
||||||
const tokenRes: any = await got
|
const tokenRes = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
corpid,
|
corpid,
|
||||||
corpsecret,
|
corpsecret,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
|
|
||||||
let options: any = {
|
let options: any = {
|
||||||
msgtype: 'mpnews',
|
msgtype: 'mpnews',
|
||||||
@@ -400,8 +387,7 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(
|
||||||
.post(
|
|
||||||
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
||||||
{
|
{
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
@@ -412,8 +398,7 @@ export default class NotificationService {
|
|||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
.json();
|
|
||||||
|
|
||||||
if (res.errcode === 0) {
|
if (res.errcode === 0) {
|
||||||
return true;
|
return true;
|
||||||
@@ -455,14 +440,12 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
...json,
|
...json,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -477,13 +460,11 @@ export default class NotificationService {
|
|||||||
const { iGotPushKey } = this.params;
|
const { iGotPushKey } = this.params;
|
||||||
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
body: `title=${this.title}&content=${this.content}`,
|
body: `title=${this.title}&content=${this.content}`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
|
|
||||||
if (res.ret === 0) {
|
if (res.ret === 0) {
|
||||||
return true;
|
return true;
|
||||||
@@ -496,20 +477,33 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async pushPlus() {
|
private async pushPlus() {
|
||||||
const { pushPlusToken, pushPlusUser } = this.params;
|
const {
|
||||||
|
pushPlusToken,
|
||||||
|
pushPlusUser,
|
||||||
|
pushplusWebhook,
|
||||||
|
pushPlusTemplate,
|
||||||
|
pushplusChannel,
|
||||||
|
pushplusCallbackUrl,
|
||||||
|
pushplusTo,
|
||||||
|
} = this.params;
|
||||||
const url = `https://www.pushplus.plus/send`;
|
const url = `https://www.pushplus.plus/send`;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
let body = {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
token: `${pushPlusToken}`,
|
token: `${pushPlusToken}`,
|
||||||
title: `${this.title}`,
|
title: `${this.title}`,
|
||||||
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||||
topic: `${pushPlusUser || ''}`,
|
topic: `${pushPlusUser || ''}`,
|
||||||
|
template: `${pushPlusTemplate || 'html'}`,
|
||||||
|
channel: `${pushplusChannel || 'wechat'}`,
|
||||||
|
webhook: `${pushplusWebhook || ''}`,
|
||||||
|
callbackUrl: `${pushplusCallbackUrl || ''}`,
|
||||||
|
to: `${pushplusTo || ''}`,
|
||||||
},
|
},
|
||||||
})
|
};
|
||||||
.json();
|
|
||||||
|
const res = await httpClient.post(url, body);
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
return true;
|
return true;
|
||||||
@@ -533,8 +527,7 @@ export default class NotificationService {
|
|||||||
|
|
||||||
const url = `https://www.weplusbot.com/send`;
|
const url = `https://www.weplusbot.com/send`;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
token: `${wePlusBotToken}`,
|
token: `${wePlusBotToken}`,
|
||||||
@@ -544,8 +537,7 @@ export default class NotificationService {
|
|||||||
receiver: `${wePlusBotReceiver || ''}`,
|
receiver: `${wePlusBotReceiver || ''}`,
|
||||||
version: `${wePlusBotVersion || 'pro'}`,
|
version: `${wePlusBotVersion || 'pro'}`,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
return true;
|
return true;
|
||||||
@@ -565,16 +557,14 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(larkKey, {
|
||||||
.post(larkKey, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
msg_type: 'text',
|
msg_type: 'text',
|
||||||
content: { text: `${this.title}\n\n${this.content}` },
|
content: { text: `${this.title}\n\n${this.content}` },
|
||||||
},
|
},
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
});
|
||||||
.json();
|
|
||||||
if (res.StatusCode === 0 || res.code === 0) {
|
if (res.StatusCode === 0 || res.code === 0) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -586,7 +576,7 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async email() {
|
private async email() {
|
||||||
const { emailPass, emailService, emailUser } = this.params;
|
const { emailPass, emailService, emailUser, emailTo } = this.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
@@ -599,7 +589,7 @@ export default class NotificationService {
|
|||||||
|
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
from: `"青龙快讯" <${emailUser}>`,
|
from: `"青龙快讯" <${emailUser}>`,
|
||||||
to: `${emailUser}`,
|
to: emailTo ? emailTo.split(';') : emailUser,
|
||||||
subject: `${this.title}`,
|
subject: `${this.title}`,
|
||||||
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||||
});
|
});
|
||||||
@@ -619,7 +609,9 @@ export default class NotificationService {
|
|||||||
private async pushMe() {
|
private async pushMe() {
|
||||||
const { pushMeKey, pushMeUrl } = this.params;
|
const { pushMeKey, pushMeUrl } = this.params;
|
||||||
try {
|
try {
|
||||||
const res: any = await got.post(pushMeUrl || 'https://push.i-i.me/', {
|
const res = await httpClient.post<'text'>(
|
||||||
|
pushMeUrl || 'https://push.i-i.me/',
|
||||||
|
{
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
push_key: pushMeKey,
|
push_key: pushMeKey,
|
||||||
@@ -627,11 +619,112 @@ export default class NotificationService {
|
|||||||
content: this.content,
|
content: this.content,
|
||||||
},
|
},
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
},
|
||||||
if (res.body === 'success') {
|
);
|
||||||
|
if (res === 'success') {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.body);
|
throw new Error(res);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ntfy() {
|
||||||
|
const {
|
||||||
|
ntfyUrl,
|
||||||
|
ntfyTopic,
|
||||||
|
ntfyPriority,
|
||||||
|
ntfyToken,
|
||||||
|
ntfyUsername,
|
||||||
|
ntfyPassword,
|
||||||
|
ntfyActions,
|
||||||
|
} = this.params;
|
||||||
|
// 编码函数
|
||||||
|
const encodeRfc2047 = (text: string, charset: string = 'UTF-8'): string => {
|
||||||
|
const encodedText = Buffer.from(text).toString('base64');
|
||||||
|
return `=?${charset}?B?${encodedText}?=`;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Title: encodeRfc2047(this.title),
|
||||||
|
Priority: `${ntfyPriority || '3'}`,
|
||||||
|
Icon: 'https://qn.whyour.cn/logo.png',
|
||||||
|
};
|
||||||
|
if (ntfyToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${ntfyToken}`;
|
||||||
|
} else if (ntfyUsername && ntfyPassword) {
|
||||||
|
headers['Authorization'] = `Basic ${Buffer.from(
|
||||||
|
`${ntfyUsername}:${ntfyPassword}`,
|
||||||
|
).toString('base64')}`;
|
||||||
|
}
|
||||||
|
if (ntfyActions) {
|
||||||
|
headers['Actions'] = encodeRfc2047(ntfyActions);
|
||||||
|
}
|
||||||
|
const res = await httpClient.request(
|
||||||
|
`${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`,
|
||||||
|
{
|
||||||
|
...this.gotOption,
|
||||||
|
body: `${this.content}`,
|
||||||
|
headers: headers,
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(await res.body.text());
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async wxPusherBot() {
|
||||||
|
const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } =
|
||||||
|
this.params;
|
||||||
|
// 处理 topicIds,将分号分隔的字符串转为数组
|
||||||
|
const topicIds = wxPusherBotTopicIds
|
||||||
|
? wxPusherBotTopicIds
|
||||||
|
.split(';')
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter((id) => id)
|
||||||
|
.map((id) => parseInt(id))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// 处理 uids,将分号分隔的字符串转为数组
|
||||||
|
const uids = wxPusherBotUids
|
||||||
|
? wxPusherBotUids
|
||||||
|
.split(';')
|
||||||
|
.map((uid) => uid.trim())
|
||||||
|
.filter((uid) => uid)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// topic_ids 和 uids 至少要有一个
|
||||||
|
if (!topicIds.length && !uids.length) {
|
||||||
|
throw new Error('wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://wxpusher.zjiecode.com/api/send/message`;
|
||||||
|
try {
|
||||||
|
const res = await httpClient.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: {
|
||||||
|
appToken: wxPusherBotAppToken,
|
||||||
|
content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,
|
||||||
|
summary: this.title,
|
||||||
|
contentType: 2,
|
||||||
|
topicIds: topicIds,
|
||||||
|
uids: uids,
|
||||||
|
verifyPayType: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 1000) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.response ? error.response.body : error);
|
throw new Error(error.response ? error.response.body : error);
|
||||||
@@ -677,15 +770,16 @@ export default class NotificationService {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const res: any = await got.post(url, {
|
const res = await httpClient.request(url, {
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: data,
|
json: data,
|
||||||
headers,
|
headers,
|
||||||
|
method: 'POST',
|
||||||
});
|
});
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.body);
|
throw new Error(await res.body.text());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -704,7 +798,7 @@ export default class NotificationService {
|
|||||||
webhookContentType,
|
webhookContentType,
|
||||||
} = this.params;
|
} = this.params;
|
||||||
|
|
||||||
if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) {
|
if (!webhookUrl?.includes('$title') && !webhookBody?.includes('$title')) {
|
||||||
throw new Error('Url 或者 Body 中必须包含 $title');
|
throw new Error('Url 或者 Body 中必须包含 $title');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,15 +814,17 @@ export default class NotificationService {
|
|||||||
allowGetBody: true,
|
allowGetBody: true,
|
||||||
...bodyParam,
|
...bodyParam,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formatUrl = webhookUrl
|
const formatUrl = webhookUrl
|
||||||
?.replaceAll('$title', encodeURIComponent(this.title))
|
?.replaceAll('$title', encodeURIComponent(this.title))
|
||||||
?.replaceAll('$content', encodeURIComponent(this.content));
|
?.replaceAll('$content', encodeURIComponent(this.content));
|
||||||
const res = await got(formatUrl, options);
|
const res = await httpClient.request(formatUrl, options);
|
||||||
|
const text = await res.body.text();
|
||||||
if (String(res.statusCode).startsWith('20')) {
|
if (String(res.statusCode).startsWith('20')) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(JSON.stringify(res));
|
throw new Error(await res.body.text());
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.response ? error.response.body : error);
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
|||||||
+27
-19
@@ -1,19 +1,18 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { createRandomString } from '../config/util';
|
import { createRandomString } from '../config/util';
|
||||||
import config from '../config';
|
|
||||||
import { App, AppModel } from '../data/open';
|
import { App, AppModel } from '../data/open';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
import sequelize, { Op } from 'sequelize';
|
import sequelize, { Op } from 'sequelize';
|
||||||
|
import { shareStore } from '../shared/store';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class OpenService {
|
export default class OpenService {
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
public async findTokenByValue(token: string): Promise<App | null> {
|
public async findApps(): Promise<App[] | null> {
|
||||||
const docs = await this.find({});
|
const docs = await this.find({});
|
||||||
const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token));
|
return docs;
|
||||||
return doc[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async create(payload: App): Promise<App> {
|
public async create(payload: App): Promise<App> {
|
||||||
@@ -21,6 +20,8 @@ export default class OpenService {
|
|||||||
tab.client_id = createRandomString(12, 12);
|
tab.client_id = createRandomString(12, 12);
|
||||||
tab.client_secret = createRandomString(24, 24);
|
tab.client_secret = createRandomString(24, 24);
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
|
const apps = await this.find({});
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
return { ...doc, tokens: [] };
|
return { ...doc, tokens: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,17 +35,19 @@ export default class OpenService {
|
|||||||
name: payload.name,
|
name: payload.name,
|
||||||
scopes: payload.scopes,
|
scopes: payload.scopes,
|
||||||
id: payload.id,
|
id: payload.id,
|
||||||
} as any);
|
} as App);
|
||||||
return { ...newDoc, tokens: [] };
|
return { ...newDoc, tokens: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateDb(payload: App): Promise<App> {
|
private async updateDb(payload: Partial<App>): Promise<App> {
|
||||||
await AppModel.update(payload, { where: { id: payload.id } });
|
await AppModel.update(payload, { where: { id: payload.id } });
|
||||||
return await this.getDb({ id: payload.id });
|
const apps = await this.find({});
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
|
return apps?.find((x) => x.id === payload.id) as App;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<App> {
|
public async getDb(query: Record<string, any>): Promise<App> {
|
||||||
const doc: any = await AppModel.findOne({ where: query });
|
const doc = await AppModel.findOne({ where: query });
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw new Error(`App ${JSON.stringify(query)} not found`);
|
throw new Error(`App ${JSON.stringify(query)} not found`);
|
||||||
}
|
}
|
||||||
@@ -53,10 +56,12 @@ export default class OpenService {
|
|||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
await AppModel.destroy({ where: { id: ids } });
|
await AppModel.destroy({ where: { id: ids } });
|
||||||
|
const apps = await this.find({});
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resetSecret(id: number): Promise<App> {
|
public async resetSecret(id: number): Promise<App> {
|
||||||
const tab: any = {
|
const tab: Partial<App> = {
|
||||||
client_secret: createRandomString(24, 24),
|
client_secret: createRandomString(24, 24),
|
||||||
tokens: [],
|
tokens: [],
|
||||||
id,
|
id,
|
||||||
@@ -74,7 +79,7 @@ export default class OpenService {
|
|||||||
public async list(
|
public async list(
|
||||||
searchText: string = '',
|
searchText: string = '',
|
||||||
sort: any = {},
|
sort: any = {},
|
||||||
query: any = {},
|
query: Record<string, any> = {},
|
||||||
): Promise<App[]> {
|
): Promise<App[]> {
|
||||||
let condition = { ...query };
|
let condition = { ...query };
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
@@ -101,7 +106,7 @@ export default class OpenService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async find(query: any, sort?: any): Promise<App[]> {
|
private async find(query: Record<string, any>, sort?: any): Promise<App[]> {
|
||||||
const docs = await AppModel.findAll({ where: { ...query } });
|
const docs = await AppModel.findAll({ where: { ...query } });
|
||||||
return docs.map((x) => x.get({ plain: true }));
|
return docs.map((x) => x.get({ plain: true }));
|
||||||
}
|
}
|
||||||
@@ -135,6 +140,8 @@ export default class OpenService {
|
|||||||
{ tokens },
|
{ tokens },
|
||||||
{ where: { client_id, client_secret } },
|
{ where: { client_id, client_secret } },
|
||||||
);
|
);
|
||||||
|
const apps = await this.find({});
|
||||||
|
await shareStore.updateApps(apps);
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data: {
|
data: {
|
||||||
@@ -152,14 +159,15 @@ export default class OpenService {
|
|||||||
value: string;
|
value: string;
|
||||||
expiration: number;
|
expiration: number;
|
||||||
}> {
|
}> {
|
||||||
let systemApp = (await AppModel.findOne({
|
const apps = await shareStore.getApps();
|
||||||
where: { name: 'system' },
|
const systemApp = apps?.find((x) => x.name === 'system');
|
||||||
})) as App;
|
|
||||||
if (!systemApp) {
|
if (!systemApp) {
|
||||||
systemApp = await this.create({
|
throw new Error('system app not found');
|
||||||
name: 'system',
|
}
|
||||||
scopes: ['crons', 'system'],
|
const now = Math.round(Date.now() / 1000);
|
||||||
} as App);
|
const currentToken = systemApp.tokens?.find((x) => x.expiration > now);
|
||||||
|
if (currentToken) {
|
||||||
|
return currentToken;
|
||||||
}
|
}
|
||||||
const { data } = await this.authToken({
|
const { data } = await this.authToken({
|
||||||
client_id: systemApp.client_id,
|
client_id: systemApp.client_id,
|
||||||
|
|||||||
+46
-12
@@ -17,6 +17,7 @@ export interface ScheduleTaskType {
|
|||||||
command: string;
|
command: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
schedule?: string;
|
schedule?: string;
|
||||||
|
runOrigin: 'subscription' | 'system' | 'script';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskCallbacks {
|
export interface TaskCallbacks {
|
||||||
@@ -40,7 +41,11 @@ export default class ScheduleService {
|
|||||||
|
|
||||||
private intervalSchedule = new ToadScheduler();
|
private intervalSchedule = new ToadScheduler();
|
||||||
|
|
||||||
private maxBuffer = 200 * 1024 * 1024;
|
private taskLimitMap = {
|
||||||
|
system: 'runWithSystemLimit' as const,
|
||||||
|
script: 'runWithScriptLimit' as const,
|
||||||
|
subscription: 'runWithSubscriptionLimit' as const,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
@@ -51,12 +56,21 @@ export default class ScheduleService {
|
|||||||
schedule?: string;
|
schedule?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
command?: string;
|
command?: string;
|
||||||
|
id: string;
|
||||||
|
runOrigin: 'subscription' | 'system' | 'script';
|
||||||
},
|
},
|
||||||
completionTime: 'start' | 'end' = 'end',
|
completionTime: 'start' | 'end' = 'end',
|
||||||
) {
|
) {
|
||||||
return taskLimit.runWithCronLimit(() => {
|
const { runOrigin, ...others } = params;
|
||||||
|
|
||||||
|
return taskLimit[this.taskLimitMap[runOrigin]](others, () => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
|
this.logger.info(
|
||||||
|
`[panel][开始执行任务] 参数: ${JSON.stringify({
|
||||||
|
...others,
|
||||||
|
command,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const startTime = dayjs();
|
const startTime = dayjs();
|
||||||
@@ -90,13 +104,21 @@ export default class ScheduleService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cp.on('exit', async (code) => {
|
cp.on('exit', async (code) => {
|
||||||
|
this.logger.info(
|
||||||
|
'[panel][执行任务结束] 参数: %s, 退出码: %j',
|
||||||
|
JSON.stringify({
|
||||||
|
...others,
|
||||||
|
command,
|
||||||
|
}),
|
||||||
|
code,
|
||||||
|
);
|
||||||
const endTime = dayjs();
|
const endTime = dayjs();
|
||||||
await callbacks.onEnd?.(
|
await callbacks.onEnd?.(
|
||||||
cp,
|
cp,
|
||||||
endTime,
|
endTime,
|
||||||
endTime.diff(startTime, 'seconds'),
|
endTime.diff(startTime, 'seconds'),
|
||||||
);
|
);
|
||||||
resolve({ ...params, pid: cp.pid, code });
|
resolve({ ...others, pid: cp.pid, code });
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@@ -111,13 +133,13 @@ export default class ScheduleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createCronTask(
|
async createCronTask(
|
||||||
{ id = 0, command, name, schedule = '' }: ScheduleTaskType,
|
{ id = 0, command, name, schedule = '', runOrigin }: ScheduleTaskType,
|
||||||
callbacks?: TaskCallbacks,
|
callbacks?: TaskCallbacks,
|
||||||
runImmediately = false,
|
runImmediately = false,
|
||||||
) {
|
) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
|
'[panel][创建cron任务] 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
|
||||||
_id,
|
_id,
|
||||||
schedule,
|
schedule,
|
||||||
name,
|
name,
|
||||||
@@ -131,6 +153,8 @@ export default class ScheduleService {
|
|||||||
name,
|
name,
|
||||||
schedule,
|
schedule,
|
||||||
command,
|
command,
|
||||||
|
id: _id,
|
||||||
|
runOrigin,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -140,13 +164,15 @@ export default class ScheduleService {
|
|||||||
name,
|
name,
|
||||||
schedule,
|
schedule,
|
||||||
command,
|
command,
|
||||||
|
id: _id,
|
||||||
|
runOrigin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info('[panel][取消定时任务], 任务名: %s', name);
|
this.logger.info('[panel][取消定时任务] 任务名: %s', name);
|
||||||
if (this.scheduleStacks.has(_id)) {
|
if (this.scheduleStacks.has(_id)) {
|
||||||
this.scheduleStacks.get(_id)?.cancel();
|
this.scheduleStacks.get(_id)?.cancel();
|
||||||
this.scheduleStacks.delete(_id);
|
this.scheduleStacks.delete(_id);
|
||||||
@@ -154,14 +180,14 @@ export default class ScheduleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createIntervalTask(
|
async createIntervalTask(
|
||||||
{ id = 0, command, name = '' }: ScheduleTaskType,
|
{ id = 0, command, name = '', runOrigin }: ScheduleTaskType,
|
||||||
schedule: SimpleIntervalSchedule,
|
schedule: SimpleIntervalSchedule,
|
||||||
runImmediately = true,
|
runImmediately = true,
|
||||||
callbacks?: TaskCallbacks,
|
callbacks?: TaskCallbacks,
|
||||||
) {
|
) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s',
|
'[panel][创建interval任务] 任务ID: %s, 任务名: %s, 执行命令: %s',
|
||||||
_id,
|
_id,
|
||||||
name,
|
name,
|
||||||
command,
|
command,
|
||||||
@@ -172,11 +198,13 @@ export default class ScheduleService {
|
|||||||
this.runTask(command, callbacks, {
|
this.runTask(command, callbacks, {
|
||||||
name,
|
name,
|
||||||
command,
|
command,
|
||||||
|
id: _id,
|
||||||
|
runOrigin,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'[执行任务失败] 命令: %s, 错误信息: %j',
|
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
command,
|
command,
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
@@ -186,7 +214,7 @@ export default class ScheduleService {
|
|||||||
const job = new LongIntervalJob(
|
const job = new LongIntervalJob(
|
||||||
{ runImmediately: false, ...schedule },
|
{ runImmediately: false, ...schedule },
|
||||||
task,
|
task,
|
||||||
_id,
|
{ id: _id },
|
||||||
);
|
);
|
||||||
|
|
||||||
this.intervalSchedule.addIntervalJob(job);
|
this.intervalSchedule.addIntervalJob(job);
|
||||||
@@ -195,13 +223,19 @@ export default class ScheduleService {
|
|||||||
this.runTask(command, callbacks, {
|
this.runTask(command, callbacks, {
|
||||||
name,
|
name,
|
||||||
command,
|
command,
|
||||||
|
id: _id,
|
||||||
|
runOrigin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
||||||
const _id = this.formatId(id);
|
const _id = this.formatId(id);
|
||||||
this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name);
|
this.logger.info(
|
||||||
|
'[panel][取消interval任务] 任务ID: %s, 任务名: %s',
|
||||||
|
_id,
|
||||||
|
name,
|
||||||
|
);
|
||||||
this.intervalSchedule.removeById(_id);
|
this.intervalSchedule.removeById(_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-6
@@ -7,6 +7,7 @@ import ScheduleService, { TaskCallbacks } from './schedule';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { TASK_COMMAND } from '../config/const';
|
import { TASK_COMMAND } from '../config/const';
|
||||||
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
|
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ScriptService {
|
export default class ScriptService {
|
||||||
@@ -43,7 +44,7 @@ export default class ScriptService {
|
|||||||
const pid = await this.scheduleService.runTask(
|
const pid = await this.scheduleService.runTask(
|
||||||
`real_time=true ${command}`,
|
`real_time=true ${command}`,
|
||||||
this.taskCallbacks(filePath),
|
this.taskCallbacks(filePath),
|
||||||
{ command },
|
{ command, id: relativePath.replace(/ /g, '-'), runOrigin: 'script' },
|
||||||
'start',
|
'start',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ export default class ScriptService {
|
|||||||
public async stopScript(filePath: string, pid: number) {
|
public async stopScript(filePath: string, pid: number) {
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
|
taskLimit.removeQueuedCron(relativePath.replace(/ /g, '-'));
|
||||||
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
|
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -62,12 +64,19 @@ export default class ScriptService {
|
|||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFile(filePath: string, fileName: string) {
|
public checkFilePath(filePath: string, fileName: string) {
|
||||||
let _filePath = join(config.scriptPath, filePath, fileName);
|
const finalPath = path.resolve(config.scriptPath, filePath, fileName);
|
||||||
if (filePath.startsWith(config.dataPath)) {
|
return finalPath.startsWith(config.scriptPath) ? finalPath : '';
|
||||||
_filePath = join(filePath, fileName);
|
|
||||||
}
|
}
|
||||||
const content = await getFileContentByName(_filePath);
|
|
||||||
|
public async getFile(filePath: string, fileName: string) {
|
||||||
|
const finalPath = this.checkFilePath(filePath, fileName);
|
||||||
|
|
||||||
|
if (!finalPath) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await getFileContentByName(finalPath);
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -7,6 +7,7 @@ import { Subscription } from '../data/subscription';
|
|||||||
import { formatUrl } from '../config/subscription';
|
import { formatUrl } from '../config/subscription';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { fileExist, rmPath } from '../config/util';
|
import { fileExist, rmPath } from '../config/util';
|
||||||
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SshKeyService {
|
export default class SshKeyService {
|
||||||
@@ -25,13 +26,13 @@ export default class SshKeyService {
|
|||||||
if (_exist) {
|
if (_exist) {
|
||||||
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
|
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
|
||||||
} else {
|
} else {
|
||||||
await fs.writeFile(this.sshConfigFilePath, '');
|
await writeFileWithLock(this.sshConfigFilePath, '', { mode: '600' });
|
||||||
}
|
}
|
||||||
if (!config.includes(this.sshConfigHeader)) {
|
if (!config.includes(this.sshConfigHeader)) {
|
||||||
await fs.writeFile(
|
await writeFileWithLock(
|
||||||
this.sshConfigFilePath,
|
this.sshConfigFilePath,
|
||||||
`${this.sshConfigHeader}\n\n${config}`,
|
`${this.sshConfigHeader}\n\n${config}`,
|
||||||
{ encoding: 'utf-8' },
|
{ mode: '600' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,10 +42,13 @@ export default class SshKeyService {
|
|||||||
key: string,
|
key: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
|
await writeFileWithLock(
|
||||||
encoding: 'utf8',
|
path.join(this.sshPath, alias),
|
||||||
|
`${key}${os.EOL}`,
|
||||||
|
{
|
||||||
mode: '400',
|
mode: '400',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('生成私钥文件失败', error);
|
this.logger.error('生成私钥文件失败', error);
|
||||||
}
|
}
|
||||||
@@ -74,11 +78,12 @@ export default class SshKeyService {
|
|||||||
this.sshPath,
|
this.sshPath,
|
||||||
alias,
|
alias,
|
||||||
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
||||||
await fs.writeFile(
|
await writeFileWithLock(
|
||||||
`${path.join(this.sshPath, `${alias}.config`)}`,
|
`${path.join(this.sshPath, `${alias}.config`)}`,
|
||||||
config,
|
config,
|
||||||
{
|
{
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
|
mode: '600',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,7 +107,11 @@ export default class SshKeyService {
|
|||||||
await this.generateSingleSshConfig(alias, host, proxy);
|
await this.generateSingleSshConfig(alias, host, proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeSSHKey(alias: string, host: string, proxy?: string): Promise<void> {
|
public async removeSSHKey(
|
||||||
|
alias: string,
|
||||||
|
host: string,
|
||||||
|
proxy?: string,
|
||||||
|
): Promise<void> {
|
||||||
await this.removePrivateKeyFile(alias);
|
await this.removePrivateKeyFile(alias);
|
||||||
await this.removeSshConfig(alias);
|
await this.removeSshConfig(alias);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import winston from 'winston';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import {
|
import {
|
||||||
Subscription,
|
Subscription,
|
||||||
|
SubscriptionInstance,
|
||||||
SubscriptionModel,
|
SubscriptionModel,
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
} from '../data/subscription';
|
} from '../data/subscription';
|
||||||
@@ -29,6 +30,7 @@ import { LOG_END_SYMBOL } from '../config/const';
|
|||||||
import { formatCommand, formatUrl } from '../config/subscription';
|
import { formatCommand, formatUrl } from '../config/subscription';
|
||||||
import { CrontabModel } from '../data/cron';
|
import { CrontabModel } from '../data/cron';
|
||||||
import CrontabService from './cron';
|
import CrontabService from './cron';
|
||||||
|
import taskLimit from '../shared/pLimit';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionService {
|
export default class SubscriptionService {
|
||||||
@@ -40,8 +42,12 @@ export default class SubscriptionService {
|
|||||||
private crontabService: CrontabService,
|
private crontabService: CrontabService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async list(searchText?: string): Promise<Subscription[]> {
|
public async list(
|
||||||
|
searchText?: string,
|
||||||
|
ids?: string,
|
||||||
|
): Promise<SubscriptionInstance[]> {
|
||||||
let query = {};
|
let query = {};
|
||||||
|
const subIds = JSON.parse(ids || '[]');
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const reg = {
|
const reg = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
@@ -62,7 +68,7 @@ export default class SubscriptionService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await SubscriptionModel.findAll({
|
const result = await SubscriptionModel.findAll({
|
||||||
where: query,
|
where: { ...query, ...(ids ? { id: subIds } : undefined) },
|
||||||
order: [
|
order: [
|
||||||
['is_disabled', 'ASC'],
|
['is_disabled', 'ASC'],
|
||||||
['createdAt', 'DESC'],
|
['createdAt', 'DESC'],
|
||||||
@@ -87,7 +93,7 @@ export default class SubscriptionService {
|
|||||||
this.scheduleService.cancelCronTask(doc as any);
|
this.scheduleService.cancelCronTask(doc as any);
|
||||||
needCreate &&
|
needCreate &&
|
||||||
(await this.scheduleService.createCronTask(
|
(await this.scheduleService.createCronTask(
|
||||||
doc as any,
|
{ ...doc, runOrigin: 'subscription' } as any,
|
||||||
this.taskCallbacks(doc),
|
this.taskCallbacks(doc),
|
||||||
runImmediately,
|
runImmediately,
|
||||||
));
|
));
|
||||||
@@ -96,7 +102,7 @@ export default class SubscriptionService {
|
|||||||
const { type, value } = doc.interval_schedule;
|
const { type, value } = doc.interval_schedule;
|
||||||
needCreate &&
|
needCreate &&
|
||||||
(await this.scheduleService.createIntervalTask(
|
(await this.scheduleService.createIntervalTask(
|
||||||
doc as any,
|
{ ...doc, runOrigin: 'subscription' } as any,
|
||||||
{ [type]: value } as SimpleIntervalSchedule,
|
{ [type]: value } as SimpleIntervalSchedule,
|
||||||
runImmediately,
|
runImmediately,
|
||||||
this.taskCallbacks(doc),
|
this.taskCallbacks(doc),
|
||||||
@@ -202,12 +208,12 @@ export default class SubscriptionService {
|
|||||||
public async create(payload: Subscription): Promise<Subscription> {
|
public async create(payload: Subscription): Promise<Subscription> {
|
||||||
const tab = new Subscription(payload);
|
const tab = new Subscription(payload);
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
await this.handleTask(doc);
|
await this.handleTask(doc.get({ plain: true }));
|
||||||
await this.setSshConfig();
|
await this.setSshConfig();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payload: Subscription): Promise<Subscription> {
|
public async insert(payload: Subscription): Promise<SubscriptionInstance> {
|
||||||
return await SubscriptionModel.create(payload, { returning: true });
|
return await SubscriptionModel.create(payload, { returning: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +265,7 @@ export default class SubscriptionService {
|
|||||||
public async remove(ids: number[], query: { force?: boolean }) {
|
public async remove(ids: number[], query: { force?: boolean }) {
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await this.handleTask(doc, false);
|
await this.handleTask(doc.get({ plain: true }), false);
|
||||||
}
|
}
|
||||||
await SubscriptionModel.destroy({ where: { id: ids } });
|
await SubscriptionModel.destroy({ where: { id: ids } });
|
||||||
await this.setSshConfig();
|
await this.setSshConfig();
|
||||||
@@ -271,7 +277,9 @@ export default class SubscriptionService {
|
|||||||
}
|
}
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
const filePath = join(config.scriptPath, doc.alias);
|
const filePath = join(config.scriptPath, doc.alias);
|
||||||
|
const repoPath = join(config.repoPath, doc.alias);
|
||||||
await rmPath(filePath);
|
await rmPath(filePath);
|
||||||
|
await rmPath(repoPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,6 +334,8 @@ export default class SubscriptionService {
|
|||||||
name: subscription.name,
|
name: subscription.name,
|
||||||
schedule: subscription.schedule,
|
schedule: subscription.schedule,
|
||||||
command,
|
command,
|
||||||
|
id: String(subscription.id),
|
||||||
|
runOrigin: 'subscription',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +344,7 @@ export default class SubscriptionService {
|
|||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
await this.setSshConfig();
|
await this.setSshConfig();
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await this.handleTask(doc, false);
|
await this.handleTask(doc.get({ plain: true }), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +353,7 @@ export default class SubscriptionService {
|
|||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
||||||
await this.setSshConfig();
|
await this.setSshConfig();
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await this.handleTask(doc);
|
await this.handleTask(doc.get({ plain: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +384,7 @@ export default class SubscriptionService {
|
|||||||
files.map(async (x) => ({
|
files.map(async (x) => ({
|
||||||
filename: x,
|
filename: x,
|
||||||
directory: relativeDir.replace(config.logPath, ''),
|
directory: relativeDir.replace(config.logPath, ''),
|
||||||
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(),
|
time: (await fs.lstat(`${dir}/${x}`)).birthtimeMs,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
).sort((a, b) => b.time - a.time);
|
).sort((a, b) => b.time - a.time);
|
||||||
|
|||||||
+129
-22
@@ -1,13 +1,13 @@
|
|||||||
import { spawn } from 'cross-spawn';
|
import { spawn } from 'cross-spawn';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import got from 'got';
|
import { Agent, request } from 'undici';
|
||||||
import sum from 'lodash/sum';
|
import sum from 'lodash/sum';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { TASK_COMMAND } from '../config/const';
|
import { NotificationModeStringMap, TASK_COMMAND } from '../config/const';
|
||||||
import {
|
import {
|
||||||
getPid,
|
getPid,
|
||||||
killTask,
|
killTask,
|
||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
parseVersion,
|
parseVersion,
|
||||||
promiseExec,
|
promiseExec,
|
||||||
readDirs,
|
readDirs,
|
||||||
|
rmPath,
|
||||||
|
setSystemTimezone,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import {
|
import {
|
||||||
DependenceModel,
|
DependenceModel,
|
||||||
@@ -34,6 +36,7 @@ import NotificationService from './notify';
|
|||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SystemService {
|
export default class SystemService {
|
||||||
@@ -48,17 +51,21 @@ export default class SystemService {
|
|||||||
|
|
||||||
public async getSystemConfig() {
|
public async getSystemConfig() {
|
||||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||||
return doc;
|
return {
|
||||||
|
...doc,
|
||||||
|
info: { ...doc.info, timezone: doc.info?.timezone || 'Asia/Shanghai' },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
|
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
|
||||||
await SystemModel.upsert({ ...payload });
|
const { id, ...others } = payload;
|
||||||
const doc = await this.getDb({ type: payload.type });
|
await SystemModel.update(others, { where: { id } });
|
||||||
|
const doc = await this.getDb({ id });
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<SystemInfo> {
|
public async getDb(query: any): Promise<SystemInfo> {
|
||||||
const doc = await SystemModel.findOne({ where: { ...query } });
|
const doc = await SystemModel.findOne({ where: query });
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw new Error(`System ${JSON.stringify(query)} not found`);
|
throw new Error(`System ${JSON.stringify(query)} not found`);
|
||||||
}
|
}
|
||||||
@@ -90,17 +97,22 @@ export default class SystemService {
|
|||||||
info: { ...oDoc.info, ...info },
|
info: { ...oDoc.info, ...info },
|
||||||
});
|
});
|
||||||
const cron = {
|
const cron = {
|
||||||
id: result.id || NaN,
|
id: result.id as number,
|
||||||
name: '删除日志',
|
name: '删除日志',
|
||||||
command: `ql rmlog ${info.logRemoveFrequency}`,
|
command: `ql rmlog ${info.logRemoveFrequency}`,
|
||||||
|
runOrigin: 'system' as const,
|
||||||
};
|
};
|
||||||
if (oDoc.info?.logRemoveFrequency) {
|
if (oDoc.info?.logRemoveFrequency) {
|
||||||
await this.scheduleService.cancelIntervalTask(cron);
|
await this.scheduleService.cancelIntervalTask(cron);
|
||||||
}
|
}
|
||||||
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {
|
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {
|
||||||
this.scheduleService.createIntervalTask(cron, {
|
this.scheduleService.createIntervalTask(
|
||||||
|
cron,
|
||||||
|
{
|
||||||
days: info.logRemoveFrequency,
|
days: info.logRemoveFrequency,
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return { code: 200, data: info };
|
return { code: 200, data: info };
|
||||||
}
|
}
|
||||||
@@ -176,6 +188,8 @@ export default class SystemService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
command,
|
command,
|
||||||
|
id: 'update-node-mirror',
|
||||||
|
runOrigin: 'system',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -250,6 +264,8 @@ export default class SystemService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
command,
|
command,
|
||||||
|
id: 'update-linux-mirror',
|
||||||
|
runOrigin: 'system',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -260,13 +276,17 @@ export default class SystemService {
|
|||||||
|
|
||||||
let lastVersionContent;
|
let lastVersionContent;
|
||||||
try {
|
try {
|
||||||
const result = await got.get(
|
const { body } = await request(
|
||||||
`${config.lastVersionFile}?t=${Date.now()}`,
|
`${config.lastVersionFile}?t=${Date.now()}`,
|
||||||
{
|
{
|
||||||
timeout: 30000,
|
dispatcher: new Agent({
|
||||||
|
keepAliveTimeout: 30000,
|
||||||
|
keepAliveMaxTimeout: 30000,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
lastVersionContent = await parseContentVersion(result.body);
|
const text = await body.text();
|
||||||
|
lastVersionContent = parseContentVersion(text);
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
|
|
||||||
if (!lastVersionContent) {
|
if (!lastVersionContent) {
|
||||||
@@ -341,13 +361,39 @@ export default class SystemService {
|
|||||||
|
|
||||||
public async reloadSystem(target?: 'system' | 'data') {
|
public async reloadSystem(target?: 'system' | 'data') {
|
||||||
const cmd = `real_time=true ql reload ${target || ''}`;
|
const cmd = `real_time=true ql reload ${target || ''}`;
|
||||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
const cp = spawn(cmd, {
|
||||||
|
shell: '/bin/bash',
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
cp.unref();
|
cp.unref();
|
||||||
|
setTimeout(() => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async notify({ title, content }: { title: string; content: string }) {
|
public async notify({
|
||||||
const isSuccess = await this.notificationService.notify(title, content);
|
title,
|
||||||
|
content,
|
||||||
|
notificationInfo,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
notificationInfo?: NotificationInfo;
|
||||||
|
}) {
|
||||||
|
const typeString =
|
||||||
|
typeof notificationInfo?.type === 'number'
|
||||||
|
? NotificationModeStringMap[notificationInfo.type]
|
||||||
|
: undefined;
|
||||||
|
if (notificationInfo && typeString) {
|
||||||
|
notificationInfo.type = typeString;
|
||||||
|
}
|
||||||
|
const isSuccess = await this.notificationService.notify(
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
notificationInfo,
|
||||||
|
);
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
return { code: 200, message: '通知发送成功' };
|
return { code: 200, message: '通知发送成功' };
|
||||||
} else {
|
} else {
|
||||||
@@ -355,12 +401,15 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run({ command }: { command: string }, callback: TaskCallbacks) {
|
public async run({ command, logPath }: { command: string; logPath?: string }, callback: TaskCallbacks) {
|
||||||
if (!command.startsWith(TASK_COMMAND)) {
|
if (!command.startsWith(TASK_COMMAND)) {
|
||||||
command = `${TASK_COMMAND} ${command}`;
|
command = `${TASK_COMMAND} ${command}`;
|
||||||
}
|
}
|
||||||
this.scheduleService.runTask(`real_time=true ${command}`, callback, {
|
const logPathPrefix = logPath ? `real_log_path=${logPath}` : ''
|
||||||
|
this.scheduleService.runTask(`${logPathPrefix} real_time=true ${command}`, callback, {
|
||||||
command,
|
command,
|
||||||
|
id: command.replace(/ /g, '-'),
|
||||||
|
runOrigin: 'system',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,10 +435,16 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async exportData(res: Response) {
|
public async exportData(res: Response, type?: string[]) {
|
||||||
try {
|
try {
|
||||||
|
let dataDirs = ['db', 'upload'];
|
||||||
|
if (type && type.length) {
|
||||||
|
dataDirs = dataDirs.concat(type.filter((x) => x !== 'base'));
|
||||||
|
}
|
||||||
|
const dataPaths = dataDirs.map((dir) => `data/${dir}`);
|
||||||
await promiseExec(
|
await promiseExec(
|
||||||
`cd ${config.rootPath} && tar -zcvf ${config.dataTgzFile} data/`,
|
`cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile
|
||||||
|
} ${dataPaths.join(' ')}`,
|
||||||
);
|
);
|
||||||
res.download(config.dataTgzFile);
|
res.download(config.dataTgzFile);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -401,7 +456,7 @@ export default class SystemService {
|
|||||||
try {
|
try {
|
||||||
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
|
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
|
||||||
const res = await promiseExec(
|
const res = await promiseExec(
|
||||||
`cd ${config.tmpPath} && tar -zxvf data.tgz`,
|
`cd ${config.tmpPath} && tar -zxvf ${config.dataTgzFile}`,
|
||||||
);
|
);
|
||||||
return { code: 200, data: res };
|
return { code: 200, data: res };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -409,9 +464,25 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSystemLog(res: Response) {
|
public async getSystemLog(
|
||||||
|
res: Response,
|
||||||
|
query: {
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const startTime = dayjs(query.startTime || undefined)
|
||||||
|
.startOf('d')
|
||||||
|
.valueOf();
|
||||||
|
const endTime = dayjs(query.endTime || undefined)
|
||||||
|
.endOf('d')
|
||||||
|
.valueOf();
|
||||||
const result = await readDirs(config.systemLogPath, config.systemLogPath);
|
const result = await readDirs(config.systemLogPath, config.systemLogPath);
|
||||||
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
|
const logs = result
|
||||||
|
.reverse()
|
||||||
|
.filter((x) => x.title.endsWith('.log'))
|
||||||
|
.filter((x) => x.createTime >= startTime && x.createTime <= endTime);
|
||||||
|
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Length': sum(logs.map((x) => x.size)),
|
'Content-Length': sum(logs.map((x) => x.size)),
|
||||||
});
|
});
|
||||||
@@ -433,4 +504,40 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
})(res, logs);
|
})(res, logs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteSystemLog() {
|
||||||
|
const result = await readDirs(config.systemLogPath, config.systemLogPath);
|
||||||
|
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
|
||||||
|
for (const log of logs) {
|
||||||
|
await rmPath(path.join(config.systemLogPath, log.title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateTimezone(info: SystemModelInfo) {
|
||||||
|
if (!info.timezone) {
|
||||||
|
info.timezone = 'Asia/Shanghai';
|
||||||
|
}
|
||||||
|
const oDoc = await this.getSystemConfig();
|
||||||
|
await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
info: { ...oDoc.info, ...info },
|
||||||
|
});
|
||||||
|
const success = await setSystemTimezone(info.timezone);
|
||||||
|
if (success) {
|
||||||
|
return { code: 200, data: info };
|
||||||
|
} else {
|
||||||
|
return { code: 400, message: '设置时区失败' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cleanDependence(type: 'node' | 'python3') {
|
||||||
|
if (!type || !['node', 'python3'].includes(type)) {
|
||||||
|
return { code: 400, message: '参数错误' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const finalPath = path.join(config.dependenceCachePath, type);
|
||||||
|
await fs.promises.rm(finalPath, { recursive: true });
|
||||||
|
} catch (error) { }
|
||||||
|
return { code: 200 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-87
@@ -1,15 +1,7 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import {
|
import { createRandomString } from '../config/util';
|
||||||
createFile,
|
|
||||||
createRandomString,
|
|
||||||
fileExist,
|
|
||||||
getNetIp,
|
|
||||||
getPlatform,
|
|
||||||
safeJSONParse,
|
|
||||||
} from '../config/util';
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs/promises';
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { authenticator } from '@otplib/preset-default';
|
import { authenticator } from '@otplib/preset-default';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +10,7 @@ import {
|
|||||||
SystemModel,
|
SystemModel,
|
||||||
SystemModelInfo,
|
SystemModelInfo,
|
||||||
LoginStatus,
|
LoginStatus,
|
||||||
|
AuthInfo,
|
||||||
} from '../data/system';
|
} from '../data/system';
|
||||||
import { NotificationInfo } from '../data/notify';
|
import { NotificationInfo } from '../data/notify';
|
||||||
import NotificationService from './notify';
|
import NotificationService from './notify';
|
||||||
@@ -28,6 +21,9 @@ import dayjs from 'dayjs';
|
|||||||
import IP2Region from 'ip2region';
|
import IP2Region from 'ip2region';
|
||||||
import requestIp from 'request-ip';
|
import requestIp from 'request-ip';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
|
import pickBy from 'lodash/pickBy';
|
||||||
|
import isNil from 'lodash/isNil';
|
||||||
|
import { shareStore } from '../shared/store';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class UserService {
|
export default class UserService {
|
||||||
@@ -48,15 +44,9 @@ export default class UserService {
|
|||||||
req: Request,
|
req: Request,
|
||||||
needTwoFactor = true,
|
needTwoFactor = true,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const _exist = await fileExist(config.authConfigFile);
|
|
||||||
if (!_exist) {
|
|
||||||
return this.initAuthInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
let { username, password } = payloads;
|
let { username, password } = payloads;
|
||||||
const content = await this.getAuthInfo();
|
const content = await this.getAuthInfo();
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
if (content) {
|
|
||||||
let {
|
let {
|
||||||
username: cUsername,
|
username: cUsername,
|
||||||
password: cPassword,
|
password: cPassword,
|
||||||
@@ -65,21 +55,9 @@ export default class UserService {
|
|||||||
lastip,
|
lastip,
|
||||||
lastaddr,
|
lastaddr,
|
||||||
twoFactorActivated,
|
twoFactorActivated,
|
||||||
twoFactorActived,
|
|
||||||
tokens = {},
|
tokens = {},
|
||||||
platform,
|
platform,
|
||||||
} = content;
|
} = content;
|
||||||
// patch old field
|
|
||||||
twoFactorActivated = twoFactorActivated || twoFactorActived;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(cUsername === 'admin' && cPassword === 'admin') ||
|
|
||||||
!cUsername ||
|
|
||||||
!cPassword
|
|
||||||
) {
|
|
||||||
return this.initAuthInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
const retriesTime = Math.pow(3, retries) * 1000;
|
const retriesTime = Math.pow(3, retries) * 1000;
|
||||||
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
||||||
const waitTime = Math.ceil(
|
const waitTime = Math.ceil(
|
||||||
@@ -98,7 +76,7 @@ export default class UserService {
|
|||||||
twoFactorActivated &&
|
twoFactorActivated &&
|
||||||
needTwoFactor
|
needTwoFactor
|
||||||
) {
|
) {
|
||||||
this.updateAuthInfo(content, {
|
await this.updateAuthInfo(content, {
|
||||||
isTwoFactorChecking: true,
|
isTwoFactorChecking: true,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -113,19 +91,17 @@ export default class UserService {
|
|||||||
let address = '';
|
let address = '';
|
||||||
if (ipAddress) {
|
if (ipAddress) {
|
||||||
const { country, province, city, isp } = ipAddress;
|
const { country, province, city, isp } = ipAddress;
|
||||||
address = uniq([country, province, city, isp])
|
address = uniq([country, province, city, isp]).filter(Boolean).join(' ');
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
}
|
}
|
||||||
if (username === cUsername && password === cPassword) {
|
if (username === cUsername && password === cPassword) {
|
||||||
const data = createRandomString(50, 100);
|
const data = createRandomString(50, 100);
|
||||||
const expiration = twoFactorActivated ? 60 : 20;
|
const expiration = twoFactorActivated ? '60d' : '20d';
|
||||||
let token = jwt.sign({ data }, config.secret as any, {
|
let token = jwt.sign({ data }, config.jwt.secret, {
|
||||||
expiresIn: 60 * 60 * 24 * expiration,
|
expiresIn: config.jwt.expiresIn || expiration,
|
||||||
algorithm: 'HS384',
|
algorithm: 'HS384',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateAuthInfo(content, {
|
await this.updateAuthInfo(content, {
|
||||||
token,
|
token,
|
||||||
tokens: {
|
tokens: {
|
||||||
...tokens,
|
...tokens,
|
||||||
@@ -154,12 +130,20 @@ export default class UserService {
|
|||||||
status: LoginStatus.success,
|
status: LoginStatus.success,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.getLoginLog();
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
data: {
|
||||||
|
token,
|
||||||
|
lastip,
|
||||||
|
lastaddr,
|
||||||
|
lastlogon,
|
||||||
|
retries,
|
||||||
|
platform,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.updateAuthInfo(content, {
|
await this.updateAuthInfo(content, {
|
||||||
retries: retries + 1,
|
retries: retries + 1,
|
||||||
lastlogon: timestamp,
|
lastlogon: timestamp,
|
||||||
lastip: ip,
|
lastip: ip,
|
||||||
@@ -182,6 +166,7 @@ export default class UserService {
|
|||||||
status: LoginStatus.fail,
|
status: LoginStatus.fail,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.getLoginLog();
|
||||||
if (retries > 2) {
|
if (retries > 2) {
|
||||||
const waitTime = Math.round(Math.pow(3, retries + 1));
|
const waitTime = Math.round(Math.pow(3, retries + 1));
|
||||||
return {
|
return {
|
||||||
@@ -193,14 +178,11 @@ export default class UserService {
|
|||||||
return { code: 400, message: config.authError };
|
return { code: 400, message: config.authError };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return this.initAuthInfo();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout(platform: string): Promise<any> {
|
public async logout(platform: string): Promise<any> {
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
this.updateAuthInfo(authInfo, {
|
await this.updateAuthInfo(authInfo, {
|
||||||
token: '',
|
token: '',
|
||||||
tokens: { ...authInfo.tokens, [platform]: '' },
|
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||||
});
|
});
|
||||||
@@ -215,8 +197,9 @@ export default class UserService {
|
|||||||
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
|
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
|
||||||
);
|
);
|
||||||
if (result.length > 100) {
|
if (result.length > 100) {
|
||||||
|
const ids = result.slice(100).map((x) => x.id!);
|
||||||
await SystemModel.destroy({
|
await SystemModel.destroy({
|
||||||
where: { id: result[result.length - 1].id },
|
where: { id: ids },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result.map((x) => x.info);
|
return result.map((x) => x.info);
|
||||||
@@ -229,20 +212,6 @@ export default class UserService {
|
|||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initAuthInfo() {
|
|
||||||
await fs.writeFile(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: 'admin',
|
|
||||||
password: 'admin',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
code: 100,
|
|
||||||
message: '未找到认证文件,重新初始化',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateUsernameAndPassword({
|
public async updateUsernameAndPassword({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
@@ -254,35 +223,21 @@ export default class UserService {
|
|||||||
return { code: 400, message: '密码不能设置为admin' };
|
return { code: 400, message: '密码不能设置为admin' };
|
||||||
}
|
}
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
this.updateAuthInfo(authInfo, { username, password });
|
await this.updateAuthInfo(authInfo, { username, password });
|
||||||
return { code: 200, message: '更新成功' };
|
return { code: 200, message: '更新成功' };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAvatar(avatar: string) {
|
public async updateAvatar(avatar: string) {
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
this.updateAuthInfo(authInfo, { avatar });
|
await this.updateAuthInfo(authInfo, { avatar });
|
||||||
return { code: 200, data: avatar, message: '更新成功' };
|
return { code: 200, data: avatar, message: '更新成功' };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserInfo(): Promise<any> {
|
|
||||||
const authFileExist = await fileExist(config.authConfigFile);
|
|
||||||
if (!authFileExist) {
|
|
||||||
await createFile(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: 'admin',
|
|
||||||
password: 'admin',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return await this.getAuthInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async initTwoFactor() {
|
public async initTwoFactor() {
|
||||||
const secret = authenticator.generateSecret();
|
const secret = authenticator.generateSecret();
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
||||||
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
await this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||||
return { secret, url: otpauth };
|
return { secret, url: otpauth };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +248,7 @@ export default class UserService {
|
|||||||
secret: authInfo.twoFactorSecret,
|
secret: authInfo.twoFactorSecret,
|
||||||
});
|
});
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
await this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
||||||
}
|
}
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
@@ -318,8 +273,17 @@ export default class UserService {
|
|||||||
if (isValid) {
|
if (isValid) {
|
||||||
return this.login({ username, password }, req, false);
|
return this.login({ username, password }, req, false);
|
||||||
} else {
|
} else {
|
||||||
const { ip, address } = await getNetIp(req);
|
const ip = requestIp.getClientIp(req) || '';
|
||||||
this.updateAuthInfo(authInfo, {
|
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(' ');
|
||||||
|
}
|
||||||
|
await this.updateAuthInfo(authInfo, {
|
||||||
lastip: ip,
|
lastip: ip,
|
||||||
lastaddr: address,
|
lastaddr: address,
|
||||||
platform: req.platform,
|
platform: req.platform,
|
||||||
@@ -330,24 +294,29 @@ export default class UserService {
|
|||||||
|
|
||||||
public async deactiveTwoFactor() {
|
public async deactiveTwoFactor() {
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
this.updateAuthInfo(authInfo, {
|
await this.updateAuthInfo(authInfo, {
|
||||||
twoFactorActivated: false,
|
twoFactorActivated: false,
|
||||||
twoFactorActived: false,
|
|
||||||
twoFactorSecret: '',
|
twoFactorSecret: '',
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAuthInfo() {
|
public async getAuthInfo() {
|
||||||
const content = await fs.readFile(config.authConfigFile, 'utf8');
|
const authInfo = await shareStore.getAuthInfo();
|
||||||
return safeJSONParse(content);
|
if (authInfo) {
|
||||||
|
return authInfo;
|
||||||
|
}
|
||||||
|
const doc = await this.getDb({ type: AuthDataType.authConfig });
|
||||||
|
return (doc.info || {}) as AuthInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateAuthInfo(authInfo: any, info: any) {
|
private async updateAuthInfo(authInfo: AuthInfo, info: Partial<AuthInfo>) {
|
||||||
await fs.writeFile(
|
const result = { ...authInfo, ...info };
|
||||||
config.authConfigFile,
|
await shareStore.updateAuthInfo(result);
|
||||||
JSON.stringify({ ...authInfo, ...info }),
|
await this.updateAuthDb({
|
||||||
);
|
type: AuthDataType.authConfig,
|
||||||
|
info: result,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNotificationMode(): Promise<NotificationInfo> {
|
public async getNotificationMode(): Promise<NotificationInfo> {
|
||||||
@@ -356,7 +325,7 @@ export default class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateAuthDb(payload: SystemInfo): Promise<any> {
|
private async updateAuthDb(payload: SystemInfo): Promise<any> {
|
||||||
let doc = await SystemModel.findOne({ type: payload.type });
|
let doc = await SystemModel.findOne({ where: { type: payload.type } });
|
||||||
if (doc) {
|
if (doc) {
|
||||||
const updateResult = await SystemModel.update(payload, {
|
const updateResult = await SystemModel.update(payload, {
|
||||||
where: { id: doc.id },
|
where: { id: doc.id },
|
||||||
@@ -394,4 +363,20 @@ export default class UserService {
|
|||||||
return { code: 400, message: '通知发送失败,请检查参数' };
|
return { code: 400, message: '通知发送失败,请检查参数' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async resetAuthInfo(info: Partial<AuthInfo>) {
|
||||||
|
const { retries, twoFactorActivated, password, username } = info;
|
||||||
|
const authInfo = await this.getAuthInfo();
|
||||||
|
const payload = pickBy(
|
||||||
|
{
|
||||||
|
retries,
|
||||||
|
twoFactorActivated,
|
||||||
|
password,
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
(x) => !isNil(x),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.updateAuthInfo(authInfo, payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Dependence } from '../data/dependence';
|
||||||
|
import { ICron } from '../protos/cron';
|
||||||
|
|
||||||
|
export type Override<
|
||||||
|
T,
|
||||||
|
K extends Partial<{ [P in keyof T]: any }> | string,
|
||||||
|
> = K extends string
|
||||||
|
? Omit<T, K> & { [P in keyof T]: T[P] | unknown }
|
||||||
|
: Omit<T, keyof K> & K;
|
||||||
|
|
||||||
|
export type TCron = Override<Partial<ICron>, { id: string }>;
|
||||||
|
|
||||||
|
export interface IDependencyFn<T> {
|
||||||
|
(): Promise<T>;
|
||||||
|
dependency?: Dependence;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICronFn<T> {
|
||||||
|
(): Promise<T>;
|
||||||
|
cron?: TCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISchedule {
|
||||||
|
schedule?: string;
|
||||||
|
name?: string;
|
||||||
|
command?: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IScheduleFn<T> {
|
||||||
|
(): Promise<T>;
|
||||||
|
schedule?: ISchedule;
|
||||||
|
}
|
||||||
+106
-5
@@ -3,14 +3,23 @@ import os from 'os';
|
|||||||
import { AuthDataType, SystemModel } from '../data/system';
|
import { AuthDataType, SystemModel } from '../data/system';
|
||||||
import Logger from '../loaders/logger';
|
import Logger from '../loaders/logger';
|
||||||
import { Dependence } from '../data/dependence';
|
import { Dependence } from '../data/dependence';
|
||||||
|
import NotificationService from '../services/notify';
|
||||||
|
import {
|
||||||
|
ICronFn,
|
||||||
|
IDependencyFn,
|
||||||
|
ISchedule,
|
||||||
|
IScheduleFn,
|
||||||
|
TCron,
|
||||||
|
} from './interface';
|
||||||
|
import config from '../config';
|
||||||
|
import { credentials } from '@grpc/grpc-js';
|
||||||
|
import { ApiClient } from '../protos/api';
|
||||||
|
|
||||||
interface IDependencyFn<T> {
|
|
||||||
(): Promise<T>;
|
|
||||||
dependency?: Dependence;
|
|
||||||
}
|
|
||||||
class TaskLimit {
|
class TaskLimit {
|
||||||
private dependenyLimit = new PQueue({ concurrency: 1 });
|
private dependenyLimit = new PQueue({ concurrency: 1 });
|
||||||
private queuedDependencyIds = new Set<number>([]);
|
private queuedDependencyIds = new Set<number>([]);
|
||||||
|
private queuedCrons = new Map<string, ICronFn<any>[]>();
|
||||||
|
private repeatCronNotifyMap = new Map<string, number>();
|
||||||
private updateLogLimit = new PQueue({ concurrency: 1 });
|
private updateLogLimit = new PQueue({ concurrency: 1 });
|
||||||
private cronLimit = new PQueue({
|
private cronLimit = new PQueue({
|
||||||
concurrency: Math.max(os.cpus().length, 4),
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
@@ -18,6 +27,20 @@ class TaskLimit {
|
|||||||
private manualCronoLimit = new PQueue({
|
private manualCronoLimit = new PQueue({
|
||||||
concurrency: Math.max(os.cpus().length, 4),
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
});
|
});
|
||||||
|
private subscriptionLimit = new PQueue({
|
||||||
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
|
});
|
||||||
|
private scriptLimit = new PQueue({
|
||||||
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
|
});
|
||||||
|
private systemLimit = new PQueue({
|
||||||
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
|
});
|
||||||
|
private client = new ApiClient(
|
||||||
|
`0.0.0.0:${config.grpcPort}`,
|
||||||
|
credentials.createInsecure(),
|
||||||
|
{ 'grpc.enable_http_proxy': 0 },
|
||||||
|
);
|
||||||
|
|
||||||
get cronLimitActiveCount() {
|
get cronLimitActiveCount() {
|
||||||
return this.cronLimit.pending;
|
return this.cronLimit.pending;
|
||||||
@@ -31,6 +54,8 @@ class TaskLimit {
|
|||||||
return [...this.queuedDependencyIds.values()][0];
|
return [...this.queuedDependencyIds.values()][0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private notificationService: NotificationService = new NotificationService();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.setCustomLimit();
|
this.setCustomLimit();
|
||||||
this.handleEvents();
|
this.handleEvents();
|
||||||
@@ -71,6 +96,16 @@ class TaskLimit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeQueuedCron(id: string) {
|
||||||
|
if (this.queuedCrons.has(id)) {
|
||||||
|
const runs = this.queuedCrons.get(id);
|
||||||
|
if (runs && runs.length > 0) {
|
||||||
|
runs.pop();
|
||||||
|
this.queuedCrons.set(id, runs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async setCustomLimit(limit?: number) {
|
public async setCustomLimit(limit?: number) {
|
||||||
if (limit) {
|
if (limit) {
|
||||||
this.cronLimit.concurrency = limit;
|
this.cronLimit.concurrency = limit;
|
||||||
@@ -88,9 +123,35 @@ class TaskLimit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async runWithCronLimit<T>(
|
public async runWithCronLimit<T>(
|
||||||
fn: () => Promise<T>,
|
cron: TCron,
|
||||||
|
fn: ICronFn<T>,
|
||||||
options?: Partial<QueueAddOptions>,
|
options?: Partial<QueueAddOptions>,
|
||||||
): Promise<T | void> {
|
): Promise<T | void> {
|
||||||
|
fn.cron = cron;
|
||||||
|
let runs = this.queuedCrons.get(cron.id);
|
||||||
|
const result = runs?.length ? [...runs, fn] : [fn];
|
||||||
|
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
|
||||||
|
if (result?.length > 5) {
|
||||||
|
if (repeatTimes < 3) {
|
||||||
|
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
|
||||||
|
this.client.systemNotify(
|
||||||
|
{
|
||||||
|
title: '任务重复运行',
|
||||||
|
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
|
||||||
|
},
|
||||||
|
(err, res) => {
|
||||||
|
if (err) {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][任务重复运行] 通知失败 ${JSON.stringify(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Logger.warn(`[schedule][任务重复运行] 参数 ${JSON.stringify(cron)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.queuedCrons.set(cron.id, result);
|
||||||
return this.cronLimit.add(fn, options);
|
return this.cronLimit.add(fn, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +162,46 @@ class TaskLimit {
|
|||||||
return this.manualCronoLimit.add(fn, options);
|
return this.manualCronoLimit.add(fn, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async runWithSubscriptionLimit<T>(
|
||||||
|
schedule: TCron,
|
||||||
|
fn: IScheduleFn<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
fn.schedule = schedule;
|
||||||
|
return this.subscriptionLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runWithSystemLimit<T>(
|
||||||
|
schedule: TCron,
|
||||||
|
fn: IScheduleFn<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
fn.schedule = schedule;
|
||||||
|
return this.systemLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runWithScriptLimit<T>(
|
||||||
|
schedule: ISchedule,
|
||||||
|
fn: IScheduleFn<T>,
|
||||||
|
options?: Partial<QueueAddOptions>,
|
||||||
|
): Promise<T | void> {
|
||||||
|
fn.schedule = schedule;
|
||||||
|
return this.scriptLimit.add(fn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async waitDependencyQueueDone(): Promise<void> {
|
||||||
|
if (this.dependenyLimit.size === 0 && this.dependenyLimit.pending === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const onIdle = () => {
|
||||||
|
this.dependenyLimit.removeListener('idle', onIdle);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
this.dependenyLimit.on('idle', onIdle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public runDependeny<T>(
|
public runDependeny<T>(
|
||||||
dependency: Dependence,
|
dependency: Dependence,
|
||||||
fn: IDependencyFn<T>,
|
fn: IDependencyFn<T>,
|
||||||
|
|||||||
+19
-4
@@ -1,11 +1,17 @@
|
|||||||
import { spawn } from 'cross-spawn';
|
import { spawn } from 'cross-spawn';
|
||||||
import taskLimit from './pLimit';
|
import taskLimit from './pLimit';
|
||||||
import Logger from '../loaders/logger';
|
import Logger from '../loaders/logger';
|
||||||
|
import { ICron } from '../protos/cron';
|
||||||
|
|
||||||
export function runCron(cmd: string, options?: { schedule: string; extraSchedules: Array<{ schedule: string }>; name: string }): Promise<number | void> {
|
export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
||||||
return taskLimit.runWithCronLimit(() => {
|
return taskLimit.runWithCronLimit(cron, () => {
|
||||||
return new Promise(async (resolve: any) => {
|
return new Promise(async (resolve: any) => {
|
||||||
Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, command: cmd })}`);
|
Logger.info(
|
||||||
|
`[schedule][开始执行任务] 参数 ${JSON.stringify({
|
||||||
|
...cron,
|
||||||
|
command: cmd,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||||
|
|
||||||
cp.stderr.on('data', (data) => {
|
cp.stderr.on('data', (data) => {
|
||||||
@@ -24,7 +30,16 @@ export function runCron(cmd: string, options?: { schedule: string; extraSchedule
|
|||||||
});
|
});
|
||||||
|
|
||||||
cp.on('exit', async (code) => {
|
cp.on('exit', async (code) => {
|
||||||
resolve({ ...options, command: cmd, pid: cp.pid, code });
|
taskLimit.removeQueuedCron(cron.id);
|
||||||
|
Logger.info(
|
||||||
|
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
|
||||||
|
JSON.stringify({
|
||||||
|
...cron,
|
||||||
|
command: cmd,
|
||||||
|
}),
|
||||||
|
code,
|
||||||
|
);
|
||||||
|
resolve({ ...cron, command: cmd, pid: cp.pid, code });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { AuthInfo } from '../data/system';
|
||||||
|
import { App } from '../data/open';
|
||||||
|
import Keyv from 'keyv';
|
||||||
|
import KeyvSqlite from '@keyv/sqlite';
|
||||||
|
import config from '../config';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export enum EKeyv {
|
||||||
|
'apps' = 'apps',
|
||||||
|
'authInfo' = 'authInfo',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKeyvStore {
|
||||||
|
apps: App[];
|
||||||
|
authInfo: AuthInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyvSqlite = new KeyvSqlite(path.join(config.dbPath, 'keyv.sqlite'));
|
||||||
|
export const keyvStore = new Keyv<IKeyvStore>({ store: keyvSqlite });
|
||||||
|
|
||||||
|
export const shareStore = {
|
||||||
|
getAuthInfo() {
|
||||||
|
return keyvStore.get<IKeyvStore['authInfo']>(EKeyv.authInfo);
|
||||||
|
},
|
||||||
|
updateAuthInfo(value: IKeyvStore['authInfo']) {
|
||||||
|
return keyvStore.set<IKeyvStore['authInfo']>(EKeyv.authInfo, value);
|
||||||
|
},
|
||||||
|
getApps() {
|
||||||
|
return keyvStore.get<IKeyvStore['apps']>(EKeyv.apps);
|
||||||
|
},
|
||||||
|
updateApps(apps: App[]) {
|
||||||
|
return keyvStore.set<IKeyvStore['apps']>(EKeyv.apps, apps);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { lock } from 'proper-lockfile';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { writeFile, open, chmod } from 'fs/promises';
|
||||||
|
import { fileExist } from '../config/util';
|
||||||
|
|
||||||
|
function getUniqueLockPath(filePath: string) {
|
||||||
|
const sanitizedPath = filePath
|
||||||
|
.replace(/[<>:"/\\|?*]/g, '_')
|
||||||
|
.replace(/^_/, '');
|
||||||
|
return path.join(os.tmpdir(), `${sanitizedPath}.ql_lock`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeFileWithLock(
|
||||||
|
filePath: string,
|
||||||
|
content: string,
|
||||||
|
options: Parameters<typeof writeFile>[2] = {},
|
||||||
|
) {
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
options = { encoding: options };
|
||||||
|
}
|
||||||
|
if (!(await fileExist(filePath))) {
|
||||||
|
const fileHandle = await open(filePath, 'w');
|
||||||
|
fileHandle.close();
|
||||||
|
}
|
||||||
|
const lockfilePath = getUniqueLockPath(filePath);
|
||||||
|
|
||||||
|
const release = await lock(filePath, {
|
||||||
|
retries: {
|
||||||
|
retries: 10,
|
||||||
|
factor: 2,
|
||||||
|
minTimeout: 100,
|
||||||
|
maxTimeout: 3000,
|
||||||
|
},
|
||||||
|
lockfilePath,
|
||||||
|
});
|
||||||
|
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||||
|
if (options?.mode) {
|
||||||
|
await chmod(filePath, options.mode);
|
||||||
|
}
|
||||||
|
await release();
|
||||||
|
}
|
||||||
+2
-11
@@ -2,10 +2,10 @@ import 'reflect-metadata';
|
|||||||
import OpenService from './services/open';
|
import OpenService from './services/open';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import LoggerInstance from './loaders/logger';
|
import LoggerInstance from './loaders/logger';
|
||||||
import fs from 'fs';
|
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
import { writeFileWithLock } from './shared/utils';
|
||||||
|
|
||||||
const tokenFile = path.join(config.configPath, 'token.json');
|
const tokenFile = path.join(config.configPath, 'token.json');
|
||||||
|
|
||||||
@@ -25,16 +25,7 @@ async function getToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function writeFile(data: any) {
|
async function writeFile(data: any) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
await writeFileWithLock(tokenFile, `${JSON.stringify(data)}${os.EOL}`);
|
||||||
fs.writeFile(
|
|
||||||
tokenFile,
|
|
||||||
`${JSON.stringify(data)}${os.EOL}`,
|
|
||||||
{ encoding: 'utf8' },
|
|
||||||
() => {
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken();
|
getToken();
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"typeRoots": ["./node_modules/celebrate/lib", "./node_modules/@types"],
|
"typeRoots": [
|
||||||
"paths": {
|
"./types",
|
||||||
"@/*": ["./back/*"]
|
"../node_modules/celebrate/lib",
|
||||||
},
|
"../node_modules/@types"
|
||||||
|
],
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
@@ -16,12 +17,11 @@
|
|||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"pretty": true,
|
"pretty": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./static/build",
|
"outDir": "../static/build",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": ["./back/**/*"],
|
"include": ["./**/*"],
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"]
|
||||||
"files": ["./back/index.d.ts"]
|
|
||||||
}
|
}
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
/// <reference types="express" />
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
platform: 'desktop' | 'mobile';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import 'reflect-metadata'; // We need this in order to use @Decorators
|
|
||||||
import config from './config';
|
|
||||||
import express from 'express';
|
|
||||||
import depInjectorLoader from './loaders/depInjector';
|
|
||||||
import Logger from './loaders/logger';
|
|
||||||
|
|
||||||
|
|
||||||
async function startServer() {
|
|
||||||
const app = express();
|
|
||||||
depInjectorLoader();
|
|
||||||
|
|
||||||
await require('./loaders/update').default({ app });
|
|
||||||
|
|
||||||
app
|
|
||||||
.listen(config.updatePort, '0.0.0.0', () => {
|
|
||||||
Logger.debug(`✌️ 更新服务启动成功!`);
|
|
||||||
console.debug(`✌️ 更新服务启动成功!`);
|
|
||||||
process.send?.('ready');
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
Logger.error(err);
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
startServer();
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Joi } from 'celebrate';
|
||||||
|
import cron_parser from 'cron-parser';
|
||||||
|
import { ScheduleType } from '../interface/schedule';
|
||||||
|
|
||||||
|
const validateSchedule = (value: string, helpers: any) => {
|
||||||
|
if (
|
||||||
|
value.startsWith(ScheduleType.ONCE) ||
|
||||||
|
value.startsWith(ScheduleType.BOOT)
|
||||||
|
) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (cron_parser.parseExpression(value).hasNext()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return helpers.error('any.invalid');
|
||||||
|
}
|
||||||
|
return helpers.error('any.invalid');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scheduleSchema = Joi.string()
|
||||||
|
.required()
|
||||||
|
.custom(validateSchedule)
|
||||||
|
.messages({
|
||||||
|
'any.invalid': '无效的定时规则',
|
||||||
|
'string.empty': '定时规则不能为空',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const commonCronSchema = {
|
||||||
|
name: Joi.string().optional(),
|
||||||
|
command: Joi.string().required(),
|
||||||
|
schedule: scheduleSchema,
|
||||||
|
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),
|
||||||
|
};
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { CommandModule } from 'yargs';
|
|
||||||
export const updateCommand: CommandModule = {
|
|
||||||
command: 'update',
|
|
||||||
describe: 'Update and restart qinglong',
|
|
||||||
builder: (yargs) => {
|
|
||||||
return yargs.option('repositority', {
|
|
||||||
type: 'string',
|
|
||||||
alias: 'r',
|
|
||||||
describe: `Specify the release warehouse address of the package`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handler: async (argv) => {},
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import * as yargs from 'yargs';
|
|
||||||
import { green, red } from 'chalk';
|
|
||||||
import { updateCommand } from './commands/update';
|
|
||||||
|
|
||||||
yargs
|
|
||||||
.usage('Usage: ql [command] <options>')
|
|
||||||
.command(updateCommand)
|
|
||||||
.fail((err) => {
|
|
||||||
console.error(`${red(err)}`);
|
|
||||||
})
|
|
||||||
.alias('h', 'help')
|
|
||||||
.showHelp()
|
|
||||||
.recommendCommands().argv;
|
|
||||||
+18
-12
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.10-alpine3.18 as builder
|
FROM python:3.10-alpine3.18 AS builder
|
||||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& apk update \
|
&& apk update \
|
||||||
@@ -13,15 +13,13 @@ ARG QL_MAINTAINER="whyour"
|
|||||||
LABEL maintainer="${QL_MAINTAINER}"
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||||
ARG QL_BRANCH=develop
|
ARG QL_BRANCH=develop
|
||||||
|
ARG PYTHON_SHORT_VERSION=3.10
|
||||||
|
|
||||||
ENV PNPM_HOME=/root/.local/share/pnpm \
|
ENV QL_DIR=/ql \
|
||||||
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 \
|
QL_BRANCH=${QL_BRANCH} \
|
||||||
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 \
|
LANG=C.UTF-8 \
|
||||||
SHELL=/bin/bash \
|
SHELL=/bin/bash \
|
||||||
PS1="\u@\h:\w \$ " \
|
PS1="\u@\h:\w \$ "
|
||||||
QL_DIR=/ql \
|
|
||||||
QL_BRANCH=${QL_BRANCH}
|
|
||||||
|
|
||||||
VOLUME /ql/data
|
VOLUME /ql/data
|
||||||
|
|
||||||
@@ -41,7 +39,6 @@ RUN set -x \
|
|||||||
tzdata \
|
tzdata \
|
||||||
perl \
|
perl \
|
||||||
openssl \
|
openssl \
|
||||||
nginx \
|
|
||||||
nodejs \
|
nodejs \
|
||||||
jq \
|
jq \
|
||||||
openssh \
|
openssh \
|
||||||
@@ -53,11 +50,9 @@ RUN set -x \
|
|||||||
&& apk update \
|
&& apk update \
|
||||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
&& git config --global user.email "qinglong@users.noreply.github.com" \
|
||||||
&& git config --global user.name "qinglong" \
|
&& git config --global user.name "qinglong" \
|
||||||
&& git config --global http.postBuffer 524288000 \
|
&& git config --global http.postBuffer 524288000 \
|
||||||
&& rm -rf /root/.pnpm-store \
|
|
||||||
&& rm -rf /root/.local/share/pnpm/store \
|
|
||||||
&& rm -rf /root/.cache \
|
&& rm -rf /root/.cache \
|
||||||
&& ulimit -c 0
|
&& ulimit -c 0
|
||||||
|
|
||||||
@@ -72,11 +67,22 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
|||||||
&& cp -rf /static/* ${QL_DIR}/static \
|
&& cp -rf /static/* ${QL_DIR}/static \
|
||||||
&& rm -rf /static
|
&& rm -rf /static
|
||||||
|
|
||||||
|
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
||||||
|
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
||||||
|
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
|
||||||
|
|
||||||
|
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \
|
||||||
|
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
|
||||||
|
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
||||||
|
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||||
|
|
||||||
|
RUN pip3 install --prefix ${PYTHON_HOME} requests
|
||||||
|
|
||||||
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
WORKDIR ${QL_DIR}
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||||
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
|
CMD curl -sf --noproxy '*' http://127.0.0.1:5700/api/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
+18
-12
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-alpine3.18 as builder
|
FROM python:3.11-alpine3.18 AS builder
|
||||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& apk update \
|
&& apk update \
|
||||||
@@ -13,15 +13,13 @@ ARG QL_MAINTAINER="whyour"
|
|||||||
LABEL maintainer="${QL_MAINTAINER}"
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||||
ARG QL_BRANCH=develop
|
ARG QL_BRANCH=develop
|
||||||
|
ARG PYTHON_SHORT_VERSION=3.11
|
||||||
|
|
||||||
ENV PNPM_HOME=/root/.local/share/pnpm \
|
ENV QL_DIR=/ql \
|
||||||
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 \
|
QL_BRANCH=${QL_BRANCH} \
|
||||||
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 \
|
LANG=C.UTF-8 \
|
||||||
SHELL=/bin/bash \
|
SHELL=/bin/bash \
|
||||||
PS1="\u@\h:\w \$ " \
|
PS1="\u@\h:\w \$ "
|
||||||
QL_DIR=/ql \
|
|
||||||
QL_BRANCH=${QL_BRANCH}
|
|
||||||
|
|
||||||
VOLUME /ql/data
|
VOLUME /ql/data
|
||||||
|
|
||||||
@@ -41,7 +39,6 @@ RUN set -x \
|
|||||||
tzdata \
|
tzdata \
|
||||||
perl \
|
perl \
|
||||||
openssl \
|
openssl \
|
||||||
nginx \
|
|
||||||
nodejs \
|
nodejs \
|
||||||
jq \
|
jq \
|
||||||
openssh \
|
openssh \
|
||||||
@@ -53,11 +50,9 @@ RUN set -x \
|
|||||||
&& apk update \
|
&& apk update \
|
||||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
&& git config --global user.email "qinglong@users.noreply.github.com" \
|
||||||
&& git config --global user.name "qinglong" \
|
&& git config --global user.name "qinglong" \
|
||||||
&& git config --global http.postBuffer 524288000 \
|
&& git config --global http.postBuffer 524288000 \
|
||||||
&& rm -rf /root/.pnpm-store \
|
|
||||||
&& rm -rf /root/.local/share/pnpm/store \
|
|
||||||
&& rm -rf /root/.cache \
|
&& rm -rf /root/.cache \
|
||||||
&& ulimit -c 0
|
&& ulimit -c 0
|
||||||
|
|
||||||
@@ -72,11 +67,22 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
|||||||
&& cp -rf /static/* ${QL_DIR}/static \
|
&& cp -rf /static/* ${QL_DIR}/static \
|
||||||
&& rm -rf /static
|
&& rm -rf /static
|
||||||
|
|
||||||
|
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
||||||
|
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
||||||
|
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
|
||||||
|
|
||||||
|
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \
|
||||||
|
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
|
||||||
|
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
||||||
|
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||||
|
|
||||||
|
RUN pip3 install --prefix ${PYTHON_HOME} requests
|
||||||
|
|
||||||
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
WORKDIR ${QL_DIR}
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||||
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
|
CMD curl -sf --noproxy '*' http://127.0.0.1:5700/api/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
# alpine 基础镜像版本
|
image: whyour/qinglong:latest # 基于 Debian 的版本:whyour/qinglong:debian
|
||||||
image: whyour/qinglong:latest
|
|
||||||
# debian-slim 基础镜像版本
|
|
||||||
# image: whyour/qinglong:debian
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/ql/data
|
- ./data:/ql/data
|
||||||
ports:
|
ports:
|
||||||
- "0.0.0.0:5700:5700"
|
- "5700:5700"
|
||||||
environment:
|
environment:
|
||||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
QlBaseUrl: '/' # 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||||
QlBaseUrl: '/'
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
+13
-20
@@ -4,41 +4,34 @@ dir_shell=/ql/shell
|
|||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/env.sh
|
. $dir_shell/env.sh
|
||||||
|
|
||||||
echo -e "======================1. 检测配置文件========================\n"
|
log_with_style() {
|
||||||
|
local level="$1"
|
||||||
|
local message="$2"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
printf "\n[%s] [%7s] %s\n" "${timestamp}" "${level}" "${message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_with_style "INFO" "🚀 1. 检测配置文件..."
|
||||||
import_config "$@"
|
import_config "$@"
|
||||||
make_dir /etc/nginx/conf.d
|
|
||||||
make_dir /run/nginx
|
|
||||||
init_nginx
|
|
||||||
fix_config
|
fix_config
|
||||||
|
|
||||||
pm2 l &>/dev/null
|
pm2 l &>/dev/null
|
||||||
|
|
||||||
echo -e "======================2. 安装依赖========================\n"
|
log_with_style "INFO" "⚙️ 2. 启动 pm2 服务..."
|
||||||
patch_version
|
|
||||||
|
|
||||||
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
|
reload_pm2
|
||||||
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
echo -e "======================5. 启动bot========================\n"
|
log_with_style "INFO" "🤖 3. 启动 bot..."
|
||||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
nohup ql bot >$dir_log/bot.log 2>&1 &
|
||||||
echo -e "bot后台启动中...\n"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EnableExtraShell == true ]]; then
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
echo -e "====================6. 执行自定义脚本========================\n"
|
log_with_style "INFO" "🛠️ 4. 执行自定义脚本..."
|
||||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
nohup ql extra >$dir_log/extra.log 2>&1 &
|
||||||
echo -e "自定义脚本后台执行中...\n"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "############################################################\n"
|
log_with_style "SUCCESS" "🎉 容器启动成功!"
|
||||||
echo -e "容器启动成功..."
|
|
||||||
echo -e "############################################################\n"
|
|
||||||
|
|
||||||
crond -f >/dev/null
|
crond -f >/dev/null
|
||||||
|
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
upstream baseApi {
|
|
||||||
server 0.0.0.0:5600;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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/ {
|
|
||||||
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://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;
|
|
||||||
}
|
|
||||||
|
|
||||||
location QL_BASE_URLopen/ {
|
|
||||||
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://baseApi/open/;
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_redirect default;
|
|
||||||
proxy_connect_timeout 1800;
|
|
||||||
proxy_send_timeout 1800;
|
|
||||||
proxy_read_timeout 1800;
|
|
||||||
}
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_static on;
|
|
||||||
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_buffers 16 8k;
|
|
||||||
gzip_http_version 1.0;
|
|
||||||
QL_ROOT_CONFIG
|
|
||||||
|
|
||||||
location QL_BASE_URL_LOCATION {
|
|
||||||
QL_ALIAS_CONFIG
|
|
||||||
index index.html index.htm;
|
|
||||||
try_files $uri QL_BASE_URLindex.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ .*\.(html)$ {
|
|
||||||
add_header Cache-Control no-cache;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
user root;
|
|
||||||
worker_processes auto;
|
|
||||||
pcre_jit on;
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
include /etc/nginx/modules/*.conf;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
server_tokens off;
|
|
||||||
|
|
||||||
client_max_body_size 4096m;
|
|
||||||
client_body_buffer_size 20m;
|
|
||||||
|
|
||||||
keepalive_timeout 65;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
|
|
||||||
tcp_nodelay on;
|
|
||||||
|
|
||||||
ssl_prefer_server_ciphers on;
|
|
||||||
|
|
||||||
ssl_session_cache shared:SSL:2m;
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_static on;
|
|
||||||
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_buffers 16 8k;
|
|
||||||
gzip_http_version 1.0;
|
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
|
||||||
}
|
|
||||||
+5
-25
@@ -1,14 +1,14 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
apps: [
|
apps: [
|
||||||
{
|
{
|
||||||
name: 'schedule',
|
name: 'qinglong',
|
||||||
max_restarts: 10,
|
max_restarts: 5,
|
||||||
kill_timeout: 15000,
|
kill_timeout: 1000,
|
||||||
wait_ready: true,
|
wait_ready: true,
|
||||||
listen_timeout: 10000,
|
listen_timeout: 5000,
|
||||||
source_map_support: true,
|
source_map_support: true,
|
||||||
time: true,
|
time: true,
|
||||||
script: 'static/build/schedule/index.js',
|
script: 'static/build/app.js',
|
||||||
env: {
|
env: {
|
||||||
http_proxy: '',
|
http_proxy: '',
|
||||||
https_proxy: '',
|
https_proxy: '',
|
||||||
@@ -18,25 +18,5 @@ module.exports = {
|
|||||||
ALL_PROXY: '',
|
ALL_PROXY: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'public',
|
|
||||||
max_restarts: 10,
|
|
||||||
kill_timeout: 15000,
|
|
||||||
wait_ready: true,
|
|
||||||
listen_timeout: 10000,
|
|
||||||
source_map_support: true,
|
|
||||||
time: true,
|
|
||||||
script: 'static/build/public.js',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'panel',
|
|
||||||
max_restarts: 10,
|
|
||||||
kill_timeout: 15000,
|
|
||||||
wait_ready: true,
|
|
||||||
listen_timeout: 10000,
|
|
||||||
source_map_support: true,
|
|
||||||
time: true,
|
|
||||||
script: 'static/build/app.js',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-2
@@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
"watch": ["back", ".env"],
|
"watch": [
|
||||||
|
"back",
|
||||||
|
".env"
|
||||||
|
],
|
||||||
"ext": "js,ts,json",
|
"ext": "js,ts,json",
|
||||||
"exec": "ts-node -P tsconfig.back.json ./back/app.ts"
|
"env": {
|
||||||
|
"NODE_ENV": "development",
|
||||||
|
"TS_NODE_PROJECT": "./back/tsconfig.json"
|
||||||
|
},
|
||||||
|
"verbose": true,
|
||||||
|
"execMap": {
|
||||||
|
"ts": "node --require ts-node/register"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
apps: [
|
|
||||||
{
|
|
||||||
name: 'update',
|
|
||||||
max_restarts: 10,
|
|
||||||
kill_timeout: 15000,
|
|
||||||
wait_ready: true,
|
|
||||||
listen_timeout: 10000,
|
|
||||||
time: true,
|
|
||||||
script: 'static/build/update.js',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
+58
-57
@@ -2,18 +2,12 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently -n w: npm:start:*",
|
"start": "concurrently -n w: npm:start:*",
|
||||||
|
"start:back": "nodemon ./back/app.ts",
|
||||||
"start:front": "max dev",
|
"start:front": "max dev",
|
||||||
"start:back": "nodemon",
|
|
||||||
"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:front": "max build",
|
||||||
"build:back": "tsc -p tsconfig.back.json",
|
"build:back": "tsc -p back/tsconfig.json",
|
||||||
"panel": "npm run build:back && node static/build/app.js",
|
"panel": "npm run build:back && node static/build/app.js",
|
||||||
"schedule": "npm run build:back && node static/build/schedule/index.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,snakeToCamel=false",
|
||||||
"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}'",
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||||
"postinstall": "max setup 2>/dev/null || true",
|
"postinstall": "max setup 2>/dev/null || true",
|
||||||
"test": "umi-test",
|
"test": "umi-test",
|
||||||
@@ -54,60 +48,66 @@
|
|||||||
"react-dom": "18",
|
"react-dom": "18",
|
||||||
"dva-core": "2"
|
"dva-core": "2"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.8.13",
|
"@grpc/grpc-js": "^1.14.0",
|
||||||
|
"@grpc/proto-loader": "^0.8.0",
|
||||||
"@otplib/preset-default": "^12.0.1",
|
"@otplib/preset-default": "^12.0.1",
|
||||||
"@sentry/node": "^7.12.1",
|
"body-parser": "^1.20.3",
|
||||||
"body-parser": "^1.19.2",
|
"celebrate": "^15.0.3",
|
||||||
"celebrate": "^15.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"chokidar": "^3.5.3",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron-parser": "^4.2.1",
|
"cron-parser": "^4.9.0",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.6",
|
||||||
"dayjs": "^1.11.2",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.4.6",
|
||||||
"express": "^4.17.3",
|
"express": "^4.21.2",
|
||||||
"express-jwt": "^6.1.1",
|
"express-jwt": "^8.4.1",
|
||||||
"express-rate-limit": "^7.0.0",
|
"express-rate-limit": "^7.4.1",
|
||||||
"express-urlrewrite": "^1.4.0",
|
"express-urlrewrite": "^2.0.3",
|
||||||
"form-data": "^4.0.0",
|
"undici": "^7.9.0",
|
||||||
"got": "^11.8.2",
|
|
||||||
"hpagent": "^1.2.0",
|
"hpagent": "^1.2.0",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^3.0.3",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.1",
|
||||||
"nedb": "^1.8.0",
|
|
||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.9.16",
|
||||||
"p-queue-cjs": "7.3.4",
|
"p-queue-cjs": "7.3.4",
|
||||||
"protobufjs": "^7.3.0",
|
"@bufbuild/protobuf": "^2.10.0",
|
||||||
"pstree.remy": "^1.1.8",
|
"ps-tree": "^1.2.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.2.2",
|
||||||
"sequelize": "^6.25.5",
|
"sequelize": "^6.37.5",
|
||||||
"serve-handler": "^6.1.3",
|
|
||||||
"sockjs": "^0.3.24",
|
"sockjs": "^0.3.24",
|
||||||
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
|
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
|
||||||
"toad-scheduler": "^1.6.0",
|
"toad-scheduler": "^3.0.1",
|
||||||
"typedi": "^0.10.0",
|
"typedi": "^0.10.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^11.0.3",
|
||||||
"winston": "^3.6.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^4.7.1",
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
"yargs": "^17.3.1",
|
|
||||||
"tough-cookie": "^4.0.0",
|
|
||||||
"request-ip": "3.3.0",
|
"request-ip": "3.3.0",
|
||||||
"ip2region": "2.3.0"
|
"ip2region": "2.3.0",
|
||||||
|
"keyv": "^5.2.3",
|
||||||
|
"@keyv/sqlite": "^4.0.1",
|
||||||
|
"proper-lockfile": "^4.1.2",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"helmet": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^4.7.0",
|
"moment": "2.30.1",
|
||||||
|
"@ant-design/icons": "^5.0.1",
|
||||||
"@ant-design/pro-layout": "6.38.22",
|
"@ant-design/pro-layout": "6.38.22",
|
||||||
|
"@codemirror/view": "^6.34.1",
|
||||||
|
"@codemirror/state": "^6.4.1",
|
||||||
"@monaco-editor/react": "4.2.1",
|
"@monaco-editor/react": "4.2.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^2.0.2",
|
||||||
"@sentry/react": "^7.12.1",
|
"react-router-dom": "6.26.1",
|
||||||
"@types/body-parser": "^1.19.2",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/cross-spawn": "^6.0.2",
|
"@types/cross-spawn": "^6.0.2",
|
||||||
@@ -118,7 +118,6 @@
|
|||||||
"@types/jsonwebtoken": "^8.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/lodash": "^4.14.185",
|
"@types/lodash": "^4.14.185",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/nedb": "^1.8.12",
|
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/node-schedule": "^1.3.2",
|
"@types/node-schedule": "^1.3.2",
|
||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
@@ -131,18 +130,20 @@
|
|||||||
"@types/sockjs-client": "^1.5.1",
|
"@types/sockjs-client": "^1.5.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@types/request-ip": "0.0.41",
|
"@types/request-ip": "0.0.41",
|
||||||
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
|
"@types/ps-tree": "^1.1.6",
|
||||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||||
"@uiw/react-codemirror": "^4.21.9",
|
"@uiw/react-codemirror": "^4.21.9",
|
||||||
"@umijs/max": "^4.0.72",
|
"@umijs/max": "^4.4.4",
|
||||||
"@umijs/ssr-darkreader": "^4.9.45",
|
"@umijs/ssr-darkreader": "^4.9.45",
|
||||||
"ahooks": "^3.7.8",
|
"ahooks": "^3.7.8",
|
||||||
"ansi-to-react": "^6.1.6",
|
"ansi-to-react": "^6.1.6",
|
||||||
"antd": "^4.24.8",
|
"antd": "^4.24.16",
|
||||||
"antd-img-crop": "^4.2.3",
|
"antd-img-crop": "^4.23.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"compression-webpack-plugin": "9.2.0",
|
"compression-webpack-plugin": "9.2.0",
|
||||||
"concurrently": "^7.0.0",
|
"concurrently": "^7.0.0",
|
||||||
"react-hotkeys-hook": "^4.4.1",
|
"react-hotkeys-hook": "^4.6.1",
|
||||||
"file-saver": "2.0.2",
|
"file-saver": "2.0.2",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"monaco-editor": "0.33.0",
|
"monaco-editor": "0.33.0",
|
||||||
@@ -153,23 +154,23 @@
|
|||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
"rc-tween-one": "^3.0.6",
|
"rc-tween-one": "^3.0.6",
|
||||||
"rc-virtual-list": "3.5.3",
|
"rc-virtual-list": "3.15.0",
|
||||||
"react": "18.2.0",
|
"react": "18.3.1",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.3.1",
|
||||||
"react-intl-universal": "^2.6.21",
|
"react-intl-universal": "^2.12.0",
|
||||||
"react-split-pane": "^0.1.92",
|
"react-split-pane": "^0.1.92",
|
||||||
"sockjs-client": "^1.6.0",
|
"sockjs-client": "^1.6.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"ts-proto": "^1.146.0",
|
"ts-proto": "^2.6.1",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"vh-check": "^2.0.5",
|
"vh-check": "^2.0.5",
|
||||||
"virtualizedtableforantd4": "1.3.0",
|
"virtualizedtableforantd4": "1.3.0",
|
||||||
"webpack": "^5.70.0",
|
"@types/compression": "^1.7.2",
|
||||||
"yorkie": "^2.0.0"
|
"@types/helmet": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+5058
-6262
File diff suppressed because it is too large
Load Diff
+46
-1
@@ -117,6 +117,16 @@ export PUSH_PLUS_TOKEN=""
|
|||||||
## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码)
|
## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码)
|
||||||
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
|
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
|
||||||
export PUSH_PLUS_USER=""
|
export PUSH_PLUS_USER=""
|
||||||
|
## 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
|
||||||
|
export PUSH_PLUS_TEMPLATE="html"
|
||||||
|
## 发送渠道,支持wechat,webhook,cp,mail,sms
|
||||||
|
export PUSH_PLUS_CHANNEL="wechat"
|
||||||
|
## webhook编码,可在pushplus公众号上扩展配置出更多渠道
|
||||||
|
export PUSH_PLUS_WEBHOOK=""
|
||||||
|
## 发送结果回调地址,会把推送最终结果通知到这个地址上
|
||||||
|
export PUSH_PLUS_CALLBACKURL=""
|
||||||
|
## 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
|
||||||
|
export PUSH_PLUS_TO=""
|
||||||
|
|
||||||
## 9. 微加机器人
|
## 9. 微加机器人
|
||||||
## 官方网站:http://www.weplusbot.com
|
## 官方网站:http://www.weplusbot.com
|
||||||
@@ -175,8 +185,16 @@ export CHRONOCAT_QQ=""
|
|||||||
export CHRONOCAT_TOKEN=""
|
export CHRONOCAT_TOKEN=""
|
||||||
|
|
||||||
## 16. SMTP
|
## 16. SMTP
|
||||||
|
## JavaScript 参数
|
||||||
## 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
|
## 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
|
||||||
export SMTP_SERVICE=""
|
export SMTP_SERVICE=""
|
||||||
|
|
||||||
|
## Python 参数
|
||||||
|
## SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||||
|
export SMTP_SERVER=""
|
||||||
|
## SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
|
||||||
|
export SMTP_SSL=""
|
||||||
|
|
||||||
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
||||||
export SMTP_EMAIL=""
|
export SMTP_EMAIL=""
|
||||||
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
@@ -203,7 +221,34 @@ export FSKEY=""
|
|||||||
export QMSG_KEY=""
|
export QMSG_KEY=""
|
||||||
export QMSG_TYPE=""
|
export QMSG_TYPE=""
|
||||||
|
|
||||||
## 20. 自定义通知
|
## 20.Ntfy
|
||||||
|
## 官方文档: https://docs.ntfy.sh
|
||||||
|
## ntfy_url 填写ntfy地址,如https://ntfy.sh
|
||||||
|
## ntfy_topic 填写ntfy的消息应用topic
|
||||||
|
## ntfy_priority 填写推送消息优先级,默认为3
|
||||||
|
## ntfy_token 填写推送token,可选
|
||||||
|
## ntfy_username 填写推送用户名称,可选
|
||||||
|
## ntfy_password 填写推送用户密码,可选
|
||||||
|
## ntfy_actions 填写推送用户动作,可选
|
||||||
|
export NTFY_URL=""
|
||||||
|
export NTFY_TOPIC=""
|
||||||
|
export NTFY_PRIORITY="3"
|
||||||
|
export NTFY_TOKEN=""
|
||||||
|
export NTFY_USERNAME=""
|
||||||
|
export NTFY_PASSWORD=""
|
||||||
|
export NTFY_ACTIONS=""
|
||||||
|
|
||||||
|
## 21. wxPusher
|
||||||
|
## 官方文档: https://wxpusher.zjiecode.com/docs/
|
||||||
|
## 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||||
|
## wxPusher 的 appToken
|
||||||
|
export WXPUSHER_APP_TOKEN=""
|
||||||
|
## wxPusher 的 topicIds,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
|
export WXPUSHER_TOPIC_IDS=""
|
||||||
|
## wxPusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
|
export WXPUSHER_UIDS=""
|
||||||
|
|
||||||
|
## 22. 自定义通知
|
||||||
## 自定义通知 接收回调的URL
|
## 自定义通知 接收回调的URL
|
||||||
export WEBHOOK_URL=""
|
export WEBHOOK_URL=""
|
||||||
## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时,直接换行或者使用 $'\n' 连接多行字符串,比如 export dd="line 1"$'\n'"line 2"
|
## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时,直接换行或者使用 $'\n' 连接多行字符串,比如 export dd="line 1"$'\n'"line 2"
|
||||||
|
|||||||
+240
-48
@@ -1,7 +1,42 @@
|
|||||||
const querystring = require('node:querystring');
|
const querystring = require('node:querystring');
|
||||||
const got = require('got');
|
const { request: undiciRequest, ProxyAgent, FormData } = require('undici');
|
||||||
const timeout = 15000;
|
const timeout = 15000;
|
||||||
|
|
||||||
|
async function request(url, options = {}) {
|
||||||
|
const { json, form, body, headers = {}, ...rest } = options;
|
||||||
|
|
||||||
|
const finalHeaders = { ...headers };
|
||||||
|
let finalBody = body;
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
finalHeaders['content-type'] = 'application/json';
|
||||||
|
finalBody = JSON.stringify(json);
|
||||||
|
} else if (form) {
|
||||||
|
finalBody = form;
|
||||||
|
delete finalHeaders['content-type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undiciRequest(url, {
|
||||||
|
headers: finalHeaders,
|
||||||
|
body: finalBody,
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(url, options = {}) {
|
||||||
|
return request(url, { ...options, method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(url, options = {}) {
|
||||||
|
return request(url, { ...options, method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpClient = {
|
||||||
|
request,
|
||||||
|
post,
|
||||||
|
get,
|
||||||
|
};
|
||||||
|
|
||||||
const push_config = {
|
const push_config = {
|
||||||
HITOKOTO: true, // 启用一言(随机句子)
|
HITOKOTO: true, // 启用一言(随机句子)
|
||||||
|
|
||||||
@@ -40,9 +75,14 @@ const push_config = {
|
|||||||
CHAT_URL: '', // synology chat url
|
CHAT_URL: '', // synology chat url
|
||||||
CHAT_TOKEN: '', // synology chat token
|
CHAT_TOKEN: '', // synology chat token
|
||||||
|
|
||||||
// 官方文档:http://www.pushplus.plus/
|
// 官方文档:https://www.pushplus.plus/
|
||||||
PUSH_PLUS_TOKEN: '', // push+ 微信推送的用户令牌
|
PUSH_PLUS_TOKEN: '', // pushplus 推送的用户令牌
|
||||||
PUSH_PLUS_USER: '', // push+ 微信推送的群组编码
|
PUSH_PLUS_USER: '', // pushplus 推送的群组编码
|
||||||
|
PUSH_PLUS_TEMPLATE: 'html', // pushplus 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
|
||||||
|
PUSH_PLUS_CHANNEL: 'wechat', // pushplus 发送渠道,支持wechat,webhook,cp,mail,sms
|
||||||
|
PUSH_PLUS_WEBHOOK: '', // pushplus webhook编码,可在pushplus公众号上扩展配置出更多渠道
|
||||||
|
PUSH_PLUS_CALLBACKURL: '', // pushplus 发送结果回调地址,会把推送最终结果通知到这个地址上
|
||||||
|
PUSH_PLUS_TO: '', // pushplus 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
|
||||||
|
|
||||||
// 微加机器人,官方网站:https://www.weplusbot.com/
|
// 微加机器人,官方网站:https://www.weplusbot.com/
|
||||||
WE_PLUS_BOT_TOKEN: '', // 微加机器人的用户令牌
|
WE_PLUS_BOT_TOKEN: '', // 微加机器人的用户令牌
|
||||||
@@ -79,7 +119,8 @@ const push_config = {
|
|||||||
AIBOTK_NAME: '', // 智能微秘书 发送群名 或者好友昵称和type要对应好
|
AIBOTK_NAME: '', // 智能微秘书 发送群名 或者好友昵称和type要对应好
|
||||||
|
|
||||||
SMTP_SERVICE: '', // 邮箱服务名称,比如 126、163、Gmail、QQ 等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
|
SMTP_SERVICE: '', // 邮箱服务名称,比如 126、163、Gmail、QQ 等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
|
||||||
SMTP_EMAIL: '', // SMTP 收发件邮箱,通知将会由自己发给自己
|
SMTP_EMAIL: '', // SMTP 发件邮箱
|
||||||
|
SMTP_TO: '', // SMTP 收件邮箱,默认通知将会发给发件邮箱
|
||||||
SMTP_PASSWORD: '', // SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
SMTP_PASSWORD: '', // SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
SMTP_NAME: '', // SMTP 收发件人姓名,可随意填写
|
SMTP_NAME: '', // SMTP 收发件人姓名,可随意填写
|
||||||
|
|
||||||
@@ -95,6 +136,20 @@ const push_config = {
|
|||||||
WEBHOOK_HEADERS: '', // 自定义通知 请求头
|
WEBHOOK_HEADERS: '', // 自定义通知 请求头
|
||||||
WEBHOOK_METHOD: '', // 自定义通知 请求方法
|
WEBHOOK_METHOD: '', // 自定义通知 请求方法
|
||||||
WEBHOOK_CONTENT_TYPE: '', // 自定义通知 content-type
|
WEBHOOK_CONTENT_TYPE: '', // 自定义通知 content-type
|
||||||
|
|
||||||
|
NTFY_URL: '', // ntfy地址,如https://ntfy.sh,默认为https://ntfy.sh
|
||||||
|
NTFY_TOPIC: '', // ntfy的消息应用topic
|
||||||
|
NTFY_PRIORITY: '3', // 推送消息优先级,默认为3
|
||||||
|
NTFY_TOKEN: '', // 推送token,可选
|
||||||
|
NTFY_USERNAME: '', // 推送用户名称,可选
|
||||||
|
NTFY_PASSWORD: '', // 推送用户密码,可选
|
||||||
|
NTFY_ACTIONS: '', // 推送用户动作,可选
|
||||||
|
|
||||||
|
// 官方文档: https://wxpusher.zjiecode.com/docs/
|
||||||
|
// 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||||
|
WXPUSHER_APP_TOKEN: '', // wxpusher 的 appToken
|
||||||
|
WXPUSHER_TOPIC_IDS: '', // wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
|
WXPUSHER_UIDS: '', // wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const key in push_config) {
|
for (const key in push_config) {
|
||||||
@@ -107,9 +162,9 @@ for (const key in push_config) {
|
|||||||
const $ = {
|
const $ = {
|
||||||
post: (params, callback) => {
|
post: (params, callback) => {
|
||||||
const { url, ...others } = params;
|
const { url, ...others } = params;
|
||||||
got.post(url, others).then(
|
httpClient.post(url, others).then(
|
||||||
(res) => {
|
async (res) => {
|
||||||
let body = res.body;
|
let body = await res.body.text();
|
||||||
try {
|
try {
|
||||||
body = JSON.parse(body);
|
body = JSON.parse(body);
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
@@ -122,9 +177,9 @@ const $ = {
|
|||||||
},
|
},
|
||||||
get: (params, callback) => {
|
get: (params, callback) => {
|
||||||
const { url, ...others } = params;
|
const { url, ...others } = params;
|
||||||
got.get(url, others).then(
|
httpClient.get(url, others).then(
|
||||||
(res) => {
|
async (res) => {
|
||||||
let body = res.body;
|
let body = await res.body.text();
|
||||||
try {
|
try {
|
||||||
body = JSON.parse(body);
|
body = JSON.parse(body);
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
@@ -140,8 +195,8 @@ const $ = {
|
|||||||
|
|
||||||
async function one() {
|
async function one() {
|
||||||
const url = 'https://v1.hitokoto.cn/';
|
const url = 'https://v1.hitokoto.cn/';
|
||||||
const res = await got.get(url);
|
const res = await httpClient.request(url);
|
||||||
const body = JSON.parse(res.body);
|
const body = await res.body.json();
|
||||||
return `${body.hitokoto} ----${body.from}`;
|
return `${body.hitokoto} ----${body.from}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +279,16 @@ function serverNotify(text, desp) {
|
|||||||
if (PUSH_KEY) {
|
if (PUSH_KEY) {
|
||||||
// 微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
|
// 微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
|
||||||
desp = desp.replace(/[\n\r]/g, '\n\n');
|
desp = desp.replace(/[\n\r]/g, '\n\n');
|
||||||
|
|
||||||
|
const matchResult = PUSH_KEY.match(/^sctp(\d+)t/i);
|
||||||
const options = {
|
const options = {
|
||||||
url: PUSH_KEY.includes('SCT')
|
url:
|
||||||
? `https://sctapi.ftqq.com/${PUSH_KEY}.send`
|
matchResult && matchResult[1]
|
||||||
: `https://sc.ftqq.com/${PUSH_KEY}.send`,
|
? `https://${matchResult[1]}.push.ft07.com/send/${PUSH_KEY}.send`
|
||||||
body: `text=${text}&desp=${desp}`,
|
: `https://sctapi.ftqq.com/${PUSH_KEY}.send`,
|
||||||
|
body: `text=${encodeURIComponent(text)}&desp=${encodeURIComponent(
|
||||||
|
desp,
|
||||||
|
)}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
@@ -408,7 +468,7 @@ function tgBotNotify(text, desp) {
|
|||||||
TG_PROXY_AUTH,
|
TG_PROXY_AUTH,
|
||||||
} = push_config;
|
} = push_config;
|
||||||
if (TG_BOT_TOKEN && TG_USER_ID) {
|
if (TG_BOT_TOKEN && TG_USER_ID) {
|
||||||
const options = {
|
let options = {
|
||||||
url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
|
url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
|
||||||
json: {
|
json: {
|
||||||
chat_id: `${TG_USER_ID}`,
|
chat_id: `${TG_USER_ID}`,
|
||||||
@@ -421,21 +481,11 @@ function tgBotNotify(text, desp) {
|
|||||||
timeout,
|
timeout,
|
||||||
};
|
};
|
||||||
if (TG_PROXY_HOST && TG_PROXY_PORT) {
|
if (TG_PROXY_HOST && TG_PROXY_PORT) {
|
||||||
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
|
let agent;
|
||||||
const options = {
|
agent = new ProxyAgent({
|
||||||
keepAlive: true,
|
uri: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
|
||||||
keepAliveMsecs: 1000,
|
});
|
||||||
maxSockets: 256,
|
options.dispatcher = agent;
|
||||||
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 = {
|
|
||||||
http: httpAgent,
|
|
||||||
https: httpsAgent,
|
|
||||||
};
|
|
||||||
Object.assign(options, { agent });
|
|
||||||
}
|
}
|
||||||
$.post(options, (err, resp, data) => {
|
$.post(options, (err, resp, data) => {
|
||||||
try {
|
try {
|
||||||
@@ -759,7 +809,15 @@ function iGotNotify(text, desp, params = {}) {
|
|||||||
|
|
||||||
function pushPlusNotify(text, desp) {
|
function pushPlusNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const { PUSH_PLUS_TOKEN, PUSH_PLUS_USER } = push_config;
|
const {
|
||||||
|
PUSH_PLUS_TOKEN,
|
||||||
|
PUSH_PLUS_USER,
|
||||||
|
PUSH_PLUS_TEMPLATE,
|
||||||
|
PUSH_PLUS_CHANNEL,
|
||||||
|
PUSH_PLUS_WEBHOOK,
|
||||||
|
PUSH_PLUS_CALLBACKURL,
|
||||||
|
PUSH_PLUS_TO,
|
||||||
|
} = push_config;
|
||||||
if (PUSH_PLUS_TOKEN) {
|
if (PUSH_PLUS_TOKEN) {
|
||||||
desp = desp.replace(/[\n\r]/g, '<br>'); // 默认为html, 不支持plaintext
|
desp = desp.replace(/[\n\r]/g, '<br>'); // 默认为html, 不支持plaintext
|
||||||
const body = {
|
const body = {
|
||||||
@@ -767,6 +825,11 @@ function pushPlusNotify(text, desp) {
|
|||||||
title: `${text}`,
|
title: `${text}`,
|
||||||
content: `${desp}`,
|
content: `${desp}`,
|
||||||
topic: `${PUSH_PLUS_USER}`,
|
topic: `${PUSH_PLUS_USER}`,
|
||||||
|
template: `${PUSH_PLUS_TEMPLATE}`,
|
||||||
|
channel: `${PUSH_PLUS_CHANNEL}`,
|
||||||
|
webhook: `${PUSH_PLUS_WEBHOOK}`,
|
||||||
|
callbackUrl: `${PUSH_PLUS_CALLBACKURL}`,
|
||||||
|
to: `${PUSH_PLUS_TO}`,
|
||||||
};
|
};
|
||||||
const options = {
|
const options = {
|
||||||
url: `https://www.pushplus.plus/send`,
|
url: `https://www.pushplus.plus/send`,
|
||||||
@@ -780,7 +843,7 @@ function pushPlusNotify(text, desp) {
|
|||||||
try {
|
try {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(
|
console.log(
|
||||||
`Push+ 发送${
|
`pushplus 发送${
|
||||||
PUSH_PLUS_USER ? '一对多' : '一对一'
|
PUSH_PLUS_USER ? '一对多' : '一对一'
|
||||||
}通知消息失败😞\n`,
|
}通知消息失败😞\n`,
|
||||||
err,
|
err,
|
||||||
@@ -788,13 +851,15 @@ function pushPlusNotify(text, desp) {
|
|||||||
} else {
|
} else {
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
console.log(
|
console.log(
|
||||||
`Push+ 发送${
|
`pushplus 发送${
|
||||||
PUSH_PLUS_USER ? '一对多' : '一对一'
|
PUSH_PLUS_USER ? '一对多' : '一对一'
|
||||||
}通知消息完成🎉\n`,
|
}通知请求成功🎉,可根据流水号查询推送结果:${
|
||||||
|
data.data
|
||||||
|
}\n注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
`Push+ 发送${
|
`pushplus 发送${
|
||||||
PUSH_PLUS_USER ? '一对多' : '一对一'
|
PUSH_PLUS_USER ? '一对多' : '一对一'
|
||||||
}通知消息异常 ${data.msg}\n`,
|
}通知消息异常 ${data.msg}\n`,
|
||||||
);
|
);
|
||||||
@@ -817,7 +882,7 @@ function wePlusBotNotify(text, desp) {
|
|||||||
const { WE_PLUS_BOT_TOKEN, WE_PLUS_BOT_RECEIVER, WE_PLUS_BOT_VERSION } =
|
const { WE_PLUS_BOT_TOKEN, WE_PLUS_BOT_RECEIVER, WE_PLUS_BOT_VERSION } =
|
||||||
push_config;
|
push_config;
|
||||||
if (WE_PLUS_BOT_TOKEN) {
|
if (WE_PLUS_BOT_TOKEN) {
|
||||||
const template = 'txt';
|
let template = 'txt';
|
||||||
if (desp.length > 800) {
|
if (desp.length > 800) {
|
||||||
desp = desp.replace(/[\n\r]/g, '<br>');
|
desp = desp.replace(/[\n\r]/g, '<br>');
|
||||||
template = 'html';
|
template = 'html';
|
||||||
@@ -958,7 +1023,8 @@ function fsBotNotify(text, desp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function smtpNotify(text, desp) {
|
async function smtpNotify(text, desp) {
|
||||||
const { SMTP_EMAIL, SMTP_PASSWORD, SMTP_SERVICE, SMTP_NAME } = push_config;
|
const { SMTP_EMAIL, SMTP_TO, SMTP_PASSWORD, SMTP_SERVICE, SMTP_NAME } =
|
||||||
|
push_config;
|
||||||
if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) {
|
if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -976,7 +1042,7 @@ async function smtpNotify(text, desp) {
|
|||||||
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
|
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
from: addr,
|
from: addr,
|
||||||
to: addr,
|
to: SMTP_TO ? SMTP_TO.split(';') : addr,
|
||||||
subject: text,
|
subject: text,
|
||||||
html: `${desp.replace(/\n/g, '<br/>')}`,
|
html: `${desp.replace(/\n/g, '<br/>')}`,
|
||||||
});
|
});
|
||||||
@@ -1143,14 +1209,20 @@ function webhookNotify(text, desp) {
|
|||||||
WEBHOOK_CONTENT_TYPE,
|
WEBHOOK_CONTENT_TYPE,
|
||||||
WEBHOOK_METHOD,
|
WEBHOOK_METHOD,
|
||||||
} = push_config;
|
} = push_config;
|
||||||
if (!WEBHOOK_URL.includes('$title') && !WEBHOOK_BODY.includes('$title')) {
|
if (
|
||||||
|
!WEBHOOK_METHOD ||
|
||||||
|
!WEBHOOK_URL ||
|
||||||
|
(!WEBHOOK_URL.includes('$title') && !WEBHOOK_BODY.includes('$title'))
|
||||||
|
) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = parseHeaders(WEBHOOK_HEADERS);
|
const headers = parseHeaders(WEBHOOK_HEADERS);
|
||||||
const body = parseBody(WEBHOOK_BODY, WEBHOOK_CONTENT_TYPE, (v) =>
|
const body = parseBody(WEBHOOK_BODY, WEBHOOK_CONTENT_TYPE, (v) =>
|
||||||
v?.replaceAll('$title', text)?.replaceAll('$content', desp),
|
v
|
||||||
|
?.replaceAll('$title', text?.replaceAll('\n', '\\n'))
|
||||||
|
?.replaceAll('$content', desp?.replaceAll('\n', '\\n')),
|
||||||
);
|
);
|
||||||
const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);
|
const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);
|
||||||
const options = {
|
const options = {
|
||||||
@@ -1162,22 +1234,140 @@ function webhookNotify(text, desp) {
|
|||||||
retry: 1,
|
retry: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (WEBHOOK_METHOD) {
|
|
||||||
const formatUrl = WEBHOOK_URL.replaceAll(
|
const formatUrl = WEBHOOK_URL.replaceAll(
|
||||||
'$title',
|
'$title',
|
||||||
encodeURIComponent(text),
|
encodeURIComponent(text),
|
||||||
).replaceAll('$content', encodeURIComponent(desp));
|
).replaceAll('$content', encodeURIComponent(desp));
|
||||||
got(formatUrl, options).then((resp) => {
|
httpClient.request(formatUrl, options).then(async (resp) => {
|
||||||
|
const body = await resp.body.text();
|
||||||
try {
|
try {
|
||||||
if (resp.statusCode !== 200) {
|
if (resp.statusCode !== 200) {
|
||||||
console.log(`自定义发送通知消息失败😞 ${resp.body}\n`);
|
console.log(`自定义发送通知消息失败😞 ${body}\n`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`自定义发送通知消息成功🎉 ${resp.body}\n`);
|
console.log(`自定义发送通知消息成功🎉 ${body}\n`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$.logErr(e, resp);
|
$.logErr(e, resp);
|
||||||
} finally {
|
} finally {
|
||||||
resolve(resp.body);
|
resolve(body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ntfyNotify(text, desp) {
|
||||||
|
function encodeRFC2047(text) {
|
||||||
|
const encodedBase64 = Buffer.from(text).toString('base64');
|
||||||
|
return `=?utf-8?B?${encodedBase64}?=`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { NTFY_URL, NTFY_TOPIC, NTFY_PRIORITY, NTFY_TOKEN, NTFY_USERNAME, NTFY_PASSWORD, NTFY_ACTIONS } = push_config;
|
||||||
|
if (NTFY_TOPIC) {
|
||||||
|
const options = {
|
||||||
|
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
|
||||||
|
body: `${desp}`,
|
||||||
|
headers: {
|
||||||
|
Title: `${encodeRFC2047(text)}`,
|
||||||
|
Priority: NTFY_PRIORITY || '3',
|
||||||
|
Icon: 'https://qn.whyour.cn/logo.png',
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
if (NTFY_TOKEN) {
|
||||||
|
options.headers['Authorization'] = `Bearer ${NTFY_TOKEN}`;
|
||||||
|
} else if (NTFY_USERNAME && NTFY_PASSWORD) {
|
||||||
|
options.headers['Authorization'] = `Basic ${Buffer.from(`${NTFY_USERNAME}:${NTFY_PASSWORD}`).toString('base64')}`;
|
||||||
|
}
|
||||||
|
if (NTFY_ACTIONS) {
|
||||||
|
options.headers['Actions'] = encodeRFC2047(NTFY_ACTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('Ntfy 通知调用API失败😞\n', err);
|
||||||
|
} else {
|
||||||
|
if (data.id) {
|
||||||
|
console.log('Ntfy 发送通知消息成功🎉\n');
|
||||||
|
} else {
|
||||||
|
console.log(`Ntfy 发送通知消息异常 ${JSON.stringify(data)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wxPusherNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { WXPUSHER_APP_TOKEN, WXPUSHER_TOPIC_IDS, WXPUSHER_UIDS } =
|
||||||
|
push_config;
|
||||||
|
if (WXPUSHER_APP_TOKEN) {
|
||||||
|
// 处理topic_ids,将分号分隔的字符串转为数组
|
||||||
|
const topicIds = WXPUSHER_TOPIC_IDS
|
||||||
|
? WXPUSHER_TOPIC_IDS.split(';')
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter((id) => id)
|
||||||
|
.map((id) => parseInt(id))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// 处理uids,将分号分隔的字符串转为数组
|
||||||
|
const uids = WXPUSHER_UIDS
|
||||||
|
? WXPUSHER_UIDS.split(';')
|
||||||
|
.map((uid) => uid.trim())
|
||||||
|
.filter((uid) => uid)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// topic_ids uids 至少有一个
|
||||||
|
if (!topicIds.length && !uids.length) {
|
||||||
|
console.log(
|
||||||
|
'wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!',
|
||||||
|
);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
appToken: WXPUSHER_APP_TOKEN,
|
||||||
|
content: `<h1>${text}</h1><br/><div style='white-space: pre-wrap;'>${desp}</div>`,
|
||||||
|
summary: text,
|
||||||
|
contentType: 2,
|
||||||
|
topicIds: topicIds,
|
||||||
|
uids: uids,
|
||||||
|
verifyPayType: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
url: 'https://wxpusher.zjiecode.com/api/send/message',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('wxpusher发送通知消息失败!\n', err);
|
||||||
|
} else {
|
||||||
|
if (data.code === 1000) {
|
||||||
|
console.log('wxpusher发送通知消息完成!');
|
||||||
|
} else {
|
||||||
|
console.log(`wxpusher发送通知消息异常:${data.msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1314,6 +1504,8 @@ async function sendNotify(text, desp, params = {}) {
|
|||||||
chronocatNotify(text, desp), // Chronocat
|
chronocatNotify(text, desp), // Chronocat
|
||||||
webhookNotify(text, desp), // 自定义通知
|
webhookNotify(text, desp), // 自定义通知
|
||||||
qmsgNotify(text, desp), // 自定义通知
|
qmsgNotify(text, desp), // 自定义通知
|
||||||
|
ntfyNotify(text, desp), // Ntfy
|
||||||
|
wxPusherNotify(text, desp), // wxpusher
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+149
-37
@@ -72,8 +72,13 @@ push_config = {
|
|||||||
'CHAT_URL': '', # synology chat url
|
'CHAT_URL': '', # synology chat url
|
||||||
'CHAT_TOKEN': '', # synology chat token
|
'CHAT_TOKEN': '', # synology chat token
|
||||||
|
|
||||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
'PUSH_PLUS_TOKEN': '', # pushplus 推送的用户令牌
|
||||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
'PUSH_PLUS_USER': '', # pushplus 推送的群组编码
|
||||||
|
'PUSH_PLUS_TEMPLATE': 'html', # pushplus 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
|
||||||
|
'PUSH_PLUS_CHANNEL': 'wechat', # pushplus 发送渠道,支持wechat,webhook,cp,mail,sms
|
||||||
|
'PUSH_PLUS_WEBHOOK': '', # pushplus webhook编码,可在pushplus公众号上扩展配置出更多渠道
|
||||||
|
'PUSH_PLUS_CALLBACKURL': '', # pushplus 发送结果回调地址,会把推送最终结果通知到这个地址上
|
||||||
|
'PUSH_PLUS_TO': '', # pushplus 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
|
||||||
|
|
||||||
'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
|
'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
|
||||||
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
|
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
|
||||||
@@ -116,7 +121,19 @@ push_config = {
|
|||||||
'WEBHOOK_BODY': '', # 自定义通知 请求体
|
'WEBHOOK_BODY': '', # 自定义通知 请求体
|
||||||
'WEBHOOK_HEADERS': '', # 自定义通知 请求头
|
'WEBHOOK_HEADERS': '', # 自定义通知 请求头
|
||||||
'WEBHOOK_METHOD': '', # 自定义通知 请求方法
|
'WEBHOOK_METHOD': '', # 自定义通知 请求方法
|
||||||
'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type
|
'WEBHOOK_CONTENT_TYPE': '', # 自定义通知 content-type
|
||||||
|
|
||||||
|
'NTFY_URL': '', # ntfy地址,如https://ntfy.sh
|
||||||
|
'NTFY_TOPIC': '', # ntfy的消息应用topic
|
||||||
|
'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3
|
||||||
|
'NTFY_TOKEN': '', # 推送token,可选
|
||||||
|
'NTFY_USERNAME': '', # 推送用户名称,可选
|
||||||
|
'NTFY_PASSWORD': '', # 推送用户密码,可选
|
||||||
|
'NTFY_ACTIONS': '', # 推送用户动作,可选
|
||||||
|
|
||||||
|
'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||||
|
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
|
'WXPUSHER_UIDS': '', # wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
}
|
}
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
@@ -131,7 +148,6 @@ def bark(title: str, content: str) -> None:
|
|||||||
使用 bark 推送消息。
|
使用 bark 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("BARK_PUSH"):
|
if not push_config.get("BARK_PUSH"):
|
||||||
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("bark 服务启动")
|
print("bark 服务启动")
|
||||||
|
|
||||||
@@ -183,7 +199,6 @@ def dingding_bot(title: str, content: str) -> None:
|
|||||||
使用 钉钉机器人 推送消息。
|
使用 钉钉机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
|
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
|
||||||
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("钉钉机器人 服务启动")
|
print("钉钉机器人 服务启动")
|
||||||
|
|
||||||
@@ -213,7 +228,6 @@ def feishu_bot(title: str, content: str) -> None:
|
|||||||
使用 飞书机器人 推送消息。
|
使用 飞书机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("FSKEY"):
|
if not push_config.get("FSKEY"):
|
||||||
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("飞书 服务启动")
|
print("飞书 服务启动")
|
||||||
|
|
||||||
@@ -232,7 +246,6 @@ def go_cqhttp(title: str, content: str) -> None:
|
|||||||
使用 go_cqhttp 推送消息。
|
使用 go_cqhttp 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
|
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
|
||||||
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("go-cqhttp 服务启动")
|
print("go-cqhttp 服务启动")
|
||||||
|
|
||||||
@@ -250,7 +263,6 @@ def gotify(title: str, content: str) -> None:
|
|||||||
使用 gotify 推送消息。
|
使用 gotify 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
||||||
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("gotify 服务启动")
|
print("gotify 服务启动")
|
||||||
|
|
||||||
@@ -273,7 +285,6 @@ def iGot(title: str, content: str) -> None:
|
|||||||
使用 iGot 推送消息。
|
使用 iGot 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("IGOT_PUSH_KEY"):
|
if not push_config.get("IGOT_PUSH_KEY"):
|
||||||
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("iGot 服务启动")
|
print("iGot 服务启动")
|
||||||
|
|
||||||
@@ -293,15 +304,18 @@ def serverJ(title: str, content: str) -> None:
|
|||||||
通过 serverJ 推送消息。
|
通过 serverJ 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("PUSH_KEY"):
|
if not push_config.get("PUSH_KEY"):
|
||||||
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("serverJ 服务启动")
|
print("serverJ 服务启动")
|
||||||
|
|
||||||
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
||||||
if push_config.get("PUSH_KEY").find("SCT") != -1:
|
|
||||||
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
match = re.match(r"sctp(\d+)t", push_config.get("PUSH_KEY"))
|
||||||
|
if match:
|
||||||
|
num = match.group(1)
|
||||||
|
url = f'https://{num}.push.ft07.com/send/{push_config.get("PUSH_KEY")}.send'
|
||||||
else:
|
else:
|
||||||
url = f'https://sc.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||||
|
|
||||||
response = requests.post(url, data=data).json()
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if response.get("errno") == 0 or response.get("code") == 0:
|
if response.get("errno") == 0 or response.get("code") == 0:
|
||||||
@@ -315,7 +329,6 @@ def pushdeer(title: str, content: str) -> None:
|
|||||||
通过PushDeer 推送消息
|
通过PushDeer 推送消息
|
||||||
"""
|
"""
|
||||||
if not push_config.get("DEER_KEY"):
|
if not push_config.get("DEER_KEY"):
|
||||||
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("PushDeer 服务启动")
|
print("PushDeer 服务启动")
|
||||||
data = {
|
data = {
|
||||||
@@ -341,7 +354,6 @@ def chat(title: str, content: str) -> None:
|
|||||||
通过Chat 推送消息
|
通过Chat 推送消息
|
||||||
"""
|
"""
|
||||||
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
|
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
|
||||||
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("chat 服务启动")
|
print("chat 服务启动")
|
||||||
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
||||||
@@ -356,26 +368,36 @@ def chat(title: str, content: str) -> None:
|
|||||||
|
|
||||||
def pushplus_bot(title: str, content: str) -> None:
|
def pushplus_bot(title: str, content: str) -> None:
|
||||||
"""
|
"""
|
||||||
通过 push+ 推送消息。
|
通过 pushplus 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("PUSH_PLUS_TOKEN"):
|
if not push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("PUSHPLUS 服务启动")
|
print("PUSHPLUS 服务启动")
|
||||||
|
|
||||||
url = "http://www.pushplus.plus/send"
|
url = "https://www.pushplus.plus/send"
|
||||||
data = {
|
data = {
|
||||||
"token": push_config.get("PUSH_PLUS_TOKEN"),
|
"token": push_config.get("PUSH_PLUS_TOKEN"),
|
||||||
"title": title,
|
"title": title,
|
||||||
"content": content,
|
"content": content,
|
||||||
"topic": push_config.get("PUSH_PLUS_USER"),
|
"topic": push_config.get("PUSH_PLUS_USER"),
|
||||||
|
"template": push_config.get("PUSH_PLUS_TEMPLATE"),
|
||||||
|
"channel": push_config.get("PUSH_PLUS_CHANNEL"),
|
||||||
|
"webhook": push_config.get("PUSH_PLUS_WEBHOOK"),
|
||||||
|
"callbackUrl": push_config.get("PUSH_PLUS_CALLBACKURL"),
|
||||||
|
"to": push_config.get("PUSH_PLUS_TO"),
|
||||||
}
|
}
|
||||||
body = json.dumps(data).encode(encoding="utf-8")
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
response = requests.post(url=url, data=body, headers=headers).json()
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
|
|
||||||
if response["code"] == 200:
|
code = response["code"]
|
||||||
print("PUSHPLUS 推送成功!")
|
if code == 200:
|
||||||
|
print("PUSHPLUS 推送请求成功,可根据流水号查询推送结果:" + response["data"])
|
||||||
|
print(
|
||||||
|
"注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果"
|
||||||
|
)
|
||||||
|
elif code == 900 or code == 903 or code == 905 or code == 999:
|
||||||
|
print(response["msg"])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
url_old = "http://pushplus.hxtrip.com/send"
|
url_old = "http://pushplus.hxtrip.com/send"
|
||||||
@@ -394,7 +416,6 @@ def weplus_bot(title: str, content: str) -> None:
|
|||||||
通过 微加机器人 推送消息。
|
通过 微加机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("WE_PLUS_BOT_TOKEN"):
|
if not push_config.get("WE_PLUS_BOT_TOKEN"):
|
||||||
print("微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("微加机器人 服务启动")
|
print("微加机器人 服务启动")
|
||||||
|
|
||||||
@@ -426,7 +447,6 @@ def qmsg_bot(title: str, content: str) -> None:
|
|||||||
使用 qmsg 推送消息。
|
使用 qmsg 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
|
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
|
||||||
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("qmsg 服务启动")
|
print("qmsg 服务启动")
|
||||||
|
|
||||||
@@ -445,11 +465,10 @@ def wecom_app(title: str, content: str) -> None:
|
|||||||
通过 企业微信 APP 推送消息。
|
通过 企业微信 APP 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("QYWX_AM"):
|
if not push_config.get("QYWX_AM"):
|
||||||
print("QYWX_AM 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
||||||
if 4 < len(QYWX_AM_AY) > 5:
|
if 4 < len(QYWX_AM_AY) > 5:
|
||||||
print("QYWX_AM 设置错误!!\n取消推送")
|
print("QYWX_AM 设置错误!!")
|
||||||
return
|
return
|
||||||
print("企业微信 APP 服务启动")
|
print("企业微信 APP 服务启动")
|
||||||
|
|
||||||
@@ -542,7 +561,6 @@ def wecom_bot(title: str, content: str) -> None:
|
|||||||
通过 企业微信机器人 推送消息。
|
通过 企业微信机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("QYWX_KEY"):
|
if not push_config.get("QYWX_KEY"):
|
||||||
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("企业微信机器人服务启动")
|
print("企业微信机器人服务启动")
|
||||||
|
|
||||||
@@ -568,7 +586,6 @@ def telegram_bot(title: str, content: str) -> None:
|
|||||||
使用 telegram 机器人 推送消息。
|
使用 telegram 机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
|
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
|
||||||
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("tg 服务启动")
|
print("tg 服务启动")
|
||||||
|
|
||||||
@@ -617,9 +634,6 @@ def aibotk(title: str, content: str) -> None:
|
|||||||
or not push_config.get("AIBOTK_TYPE")
|
or not push_config.get("AIBOTK_TYPE")
|
||||||
or not push_config.get("AIBOTK_NAME")
|
or not push_config.get("AIBOTK_NAME")
|
||||||
):
|
):
|
||||||
print(
|
|
||||||
"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
print("智能微秘书 服务启动")
|
print("智能微秘书 服务启动")
|
||||||
|
|
||||||
@@ -658,9 +672,6 @@ def smtp(title: str, content: str) -> None:
|
|||||||
or not push_config.get("SMTP_PASSWORD")
|
or not push_config.get("SMTP_PASSWORD")
|
||||||
or not push_config.get("SMTP_NAME")
|
or not push_config.get("SMTP_NAME")
|
||||||
):
|
):
|
||||||
print(
|
|
||||||
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
print("SMTP 邮件 服务启动")
|
print("SMTP 邮件 服务启动")
|
||||||
|
|
||||||
@@ -704,7 +715,6 @@ def pushme(title: str, content: str) -> None:
|
|||||||
使用 PushMe 推送消息。
|
使用 PushMe 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("PUSHME_KEY"):
|
if not push_config.get("PUSHME_KEY"):
|
||||||
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("PushMe 服务启动")
|
print("PushMe 服务启动")
|
||||||
|
|
||||||
@@ -737,7 +747,6 @@ def chronocat(title: str, content: str) -> None:
|
|||||||
or not push_config.get("CHRONOCAT_QQ")
|
or not push_config.get("CHRONOCAT_QQ")
|
||||||
or not push_config.get("CHRONOCAT_TOKEN")
|
or not push_config.get("CHRONOCAT_TOKEN")
|
||||||
):
|
):
|
||||||
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print("CHRONOCAT 服务启动")
|
print("CHRONOCAT 服务启动")
|
||||||
@@ -777,6 +786,103 @@ def chronocat(title: str, content: str) -> None:
|
|||||||
print(f"QQ群消息:{ids}推送失败!")
|
print(f"QQ群消息:{ids}推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def ntfy(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 Ntfy 推送消息
|
||||||
|
"""
|
||||||
|
|
||||||
|
def encode_rfc2047(text: str) -> str:
|
||||||
|
"""将文本编码为符合 RFC 2047 标准的格式"""
|
||||||
|
encoded_bytes = base64.b64encode(text.encode("utf-8"))
|
||||||
|
encoded_str = encoded_bytes.decode("utf-8")
|
||||||
|
return f"=?utf-8?B?{encoded_str}?="
|
||||||
|
|
||||||
|
if not push_config.get("NTFY_TOPIC"):
|
||||||
|
return
|
||||||
|
print("ntfy 服务启动")
|
||||||
|
priority = "3"
|
||||||
|
if not push_config.get("NTFY_PRIORITY"):
|
||||||
|
print("ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3")
|
||||||
|
else:
|
||||||
|
priority = push_config.get("NTFY_PRIORITY")
|
||||||
|
|
||||||
|
# 使用 RFC 2047 编码 title
|
||||||
|
encoded_title = encode_rfc2047(title)
|
||||||
|
|
||||||
|
data = content.encode(encoding="utf-8")
|
||||||
|
headers = {"Title": encoded_title, "Priority": priority, "Icon": "https://qn.whyour.cn/logo.png"} # 使用编码后的 title
|
||||||
|
if push_config.get("NTFY_TOKEN"):
|
||||||
|
headers['Authorization'] = "Bearer " + push_config.get("NTFY_TOKEN")
|
||||||
|
elif push_config.get("NTFY_USERNAME") and push_config.get("NTFY_PASSWORD"):
|
||||||
|
authStr = push_config.get("NTFY_USERNAME") + ":" + push_config.get("NTFY_PASSWORD")
|
||||||
|
headers['Authorization'] = "Basic " + base64.b64encode(authStr.encode('utf-8')).decode('utf-8')
|
||||||
|
if push_config.get("NTFY_ACTIONS"):
|
||||||
|
headers['Actions'] = encode_rfc2047(push_config.get("NTFY_ACTIONS"))
|
||||||
|
|
||||||
|
url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC")
|
||||||
|
response = requests.post(url, data=data, headers=headers)
|
||||||
|
if response.status_code == 200: # 使用 response.status_code 进行检查
|
||||||
|
print("Ntfy 推送成功!")
|
||||||
|
else:
|
||||||
|
print("Ntfy 推送失败!错误信息:", response.text)
|
||||||
|
|
||||||
|
|
||||||
|
def wxpusher_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 wxpusher 推送消息。
|
||||||
|
支持的环境变量:
|
||||||
|
- WXPUSHER_APP_TOKEN: appToken
|
||||||
|
- WXPUSHER_TOPIC_IDS: 主题ID, 多个用英文分号;分隔
|
||||||
|
- WXPUSHER_UIDS: 用户ID, 多个用英文分号;分隔
|
||||||
|
"""
|
||||||
|
if not push_config.get("WXPUSHER_APP_TOKEN"):
|
||||||
|
return
|
||||||
|
|
||||||
|
url = "https://wxpusher.zjiecode.com/api/send/message"
|
||||||
|
|
||||||
|
# 处理topic_ids和uids,将分号分隔的字符串转为数组
|
||||||
|
topic_ids = []
|
||||||
|
if push_config.get("WXPUSHER_TOPIC_IDS"):
|
||||||
|
topic_ids = [
|
||||||
|
int(id.strip())
|
||||||
|
for id in push_config.get("WXPUSHER_TOPIC_IDS").split(";")
|
||||||
|
if id.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
uids = []
|
||||||
|
if push_config.get("WXPUSHER_UIDS"):
|
||||||
|
uids = [
|
||||||
|
uid.strip()
|
||||||
|
for uid in push_config.get("WXPUSHER_UIDS").split(";")
|
||||||
|
if uid.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
# topic_ids uids 至少有一个
|
||||||
|
if not topic_ids and not uids:
|
||||||
|
print("wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("wxpusher 服务启动")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"appToken": push_config.get("WXPUSHER_APP_TOKEN"),
|
||||||
|
"content": f"<h1>{title}</h1><br/><div style='white-space: pre-wrap;'>{content}</div>",
|
||||||
|
"summary": title,
|
||||||
|
"contentType": 2,
|
||||||
|
"topicIds": topic_ids,
|
||||||
|
"uids": uids,
|
||||||
|
"verifyPayType": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
response = requests.post(url=url, json=data, headers=headers).json()
|
||||||
|
|
||||||
|
if response.get("code") == 1000:
|
||||||
|
print("wxpusher 推送成功!")
|
||||||
|
else:
|
||||||
|
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
|
||||||
|
|
||||||
|
|
||||||
def parse_headers(headers):
|
def parse_headers(headers):
|
||||||
if not headers:
|
if not headers:
|
||||||
return {}
|
return {}
|
||||||
@@ -833,7 +939,6 @@ def custom_notify(title: str, content: str) -> None:
|
|||||||
通过 自定义通知 推送消息。
|
通过 自定义通知 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
|
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
|
||||||
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print("自定义通知服务启动")
|
print("自定义通知服务启动")
|
||||||
@@ -852,7 +957,9 @@ def custom_notify(title: str, content: str) -> None:
|
|||||||
body = parse_body(
|
body = parse_body(
|
||||||
WEBHOOK_BODY,
|
WEBHOOK_BODY,
|
||||||
WEBHOOK_CONTENT_TYPE,
|
WEBHOOK_CONTENT_TYPE,
|
||||||
lambda v: v.replace("$title", title).replace("$content", content),
|
lambda v: v.replace("$title", title.replace("\n", "\\n")).replace(
|
||||||
|
"$content", content.replace("\n", "\\n")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
formatted_url = WEBHOOK_URL.replace(
|
formatted_url = WEBHOOK_URL.replace(
|
||||||
"$title", urllib.parse.quote_plus(title)
|
"$title", urllib.parse.quote_plus(title)
|
||||||
@@ -935,7 +1042,12 @@ def add_notify_function():
|
|||||||
notify_function.append(chronocat)
|
notify_function.append(chronocat)
|
||||||
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
|
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
|
||||||
notify_function.append(custom_notify)
|
notify_function.append(custom_notify)
|
||||||
|
if push_config.get("NTFY_TOPIC"):
|
||||||
|
notify_function.append(ntfy)
|
||||||
|
if push_config.get("WXPUSHER_APP_TOKEN") and (
|
||||||
|
push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS")
|
||||||
|
):
|
||||||
|
notify_function.append(wxpusher_bot)
|
||||||
if not notify_function:
|
if not notify_function:
|
||||||
print(f"无推送渠道,请检查通知变量是否正确")
|
print(f"无推送渠道,请检查通知变量是否正确")
|
||||||
return notify_function
|
return notify_function
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user